I have a working example of the web socket (ws) non secure implementation application using spring boot 1.0.0.RC5 and tomcat 8.0.3. Now i would like to switch to wss i.e. using my own self signed certificate that had been loaded by tomcat already.
This question has two parts one theoretical and one practical:
Theoretical => Do i need to have tomcat listening on two ports? i.e. on http and https. The reason i am asking this is because i read that during a web socket communication the first part of the connection is made over http and then there is so called "upgrade" to websockets. I am posting example of my test
GET /hello HTTP/1.1
Host: 127.0.0.5:8080
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en,en-gb;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
Sec-WebSocket-Key: wIKSOMgaHbEmkaRuEHZ6IA==
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
HTTP/1.1 101 Switching Protocols
Server: Apache-Coyote/1.1
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: 8trj6dyzlYdDUA3nAuwiM7SijRM=
Date: Mon, 31 Mar 2014 10:29:19 GMT
..,O.do..*i..nM,..\;..I=.
C!.U.~.U....I....-..Xu.T...H...T.E.d
.'
CONNECTED
heart-beat:0,0
version:1.1
.
.....]..M...F...f9..z?...9..{4..{4..5r...4..h/..{4..|W..
How this communication would look like over wss? Do we have the "upgrade" part as well, if so in this case we need http in order for that construct to work.
Practical => The problem i am facing is that the part of the code that is responsible for creating the stomp message is not working i.e. when i open the page
https://127.0.0.5:8888/wsTest
firefox tels me "This Connection is Untrusted" then i tell firefox "I understand the risk" and add the certificate as "Security Exception". From that point the certificate is stored under the firefox "servers" tab. All good till now. However when i change to wss this game does not work as i expected it to work. i.e. This is the function that is creating the socket on the client side.
function connect() {
var socket = new WebSocket("wss://127.0.0.5:8888/hello");
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings', function(greeting){
showGreeting(JSON.parse(greeting.body).content);
});
});
}
if i change the line
var socket = new WebSocket("wss://127.0.0.5:8888/hello");
to the http version i.e.
var socket = new WebSocket("ws://127.0.0.5:8080/hello");
then all works again. The problem seems to be with the line
stompClient.connect({}, function(frame) {
however according to this bug note (https://jira.spring.io/browse/SPR-11436) this should be the correct line
I have generated the ceritficate with the command:
keytool -genkey -alias tomcat -keyalg RSA -keystore /home/tito/Projects/syncServer/Server/certificate/sync.keystore
The server side:
@Configuration
public class TomcatEmbeded extends SpringServletContainerInitializer {
final int http_port = 8080;
final int https_port = 8888;
final String keystoreFile = "/home/tito/Projects/syncServer/Server/certificate/sync.keystore";
final String keystorePass = "changeit";
final String keystoreType = "JKS";
final String keystoreProvider = "SUN";
final String keystoreAlias = "tomcat";
final String https_scheme = "https";
final String http_scheme = "http";
@Bean
public EmbeddedServletContainerFactory servletContainer() {
TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory(http_port);
factory.setTomcatContextCustomizers(
Arrays.asList (
new TomcatContextCustomizer[]{
tomcatContextCustomizer()
}
)
);
factory.addConnectorCustomizers( new TomcatConnectorCustomizer() {
@Override
public void customize(Connector con) {
Http11NioProtocol proto = (Http11NioProtocol) con.getProtocolHandler();
try {
con.setPort(https_port);
con.setSecure(true);
con.setScheme("https");
con.setAttribute("keyAlias", keystoreAlias);
con.setAttribute("keystorePass", keystorePass.toString());
try {
con.setAttribute("keystoreFile", ResourceUtils.getFile(keystoreFile).getAbsolutePath());
} catch (FileNotFoundException e) {
throw new IllegalStateException("Cannot load keystore", e);
}
con.setAttribute("clientAuth", "false");
con.setAttribute("sslProtocol", "TLS");
con.setAttribute("SSLEnabled", true);
proto.setSSLEnabled(true);
proto.setKeystoreFile(keystoreFile);
proto.setKeystorePass(keystorePass);
proto.setKeystoreType(keystoreType);
proto.setProperty("keystoreProvider", keystoreProvider.toString());
proto.setKeyAlias(keystoreAlias);
} catch (Exception ex) {
throw new IllegalStateException("can't access keystore: [" + "keystore"
+ "] or truststore: [" + "keystore" + "]", ex);
}
System.out.println("INIT HTTPS");
}
}
);
factory.addAdditionalTomcatConnectors(httpConnector());
// factory.addErrorPages(new ErrorPage(HttpStatus.404, "/notfound.html");
System.out.println("TOMCAT CUSTOME SETTINGS INITILIZED");
return factory;
}
private Connector httpConnector() {
Connector connector = new Connector();
connector.setScheme(this.http_scheme);
connector.setSecure(true);
connector.setPort(this.http_port);
System.out.println("INIT port HTTP");
return connector;
}
@Bean
public TomcatContextCustomizer tomcatContextCustomizer() {
System.out.println("TOMCATCONTEXTCUSTOMIZER INITILIZED");
return new TomcatContextCustomizer() {
@Override
public void customize(Context context) {
// TODO Auto-generated method stub
context.addServletContainerInitializer(new WsSci(), null);
}
};
}
here is the web socket configuration portion
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/hello");
}
@Override
public void configureClientInboundChannel(ChannelRegistration channelRegistration) {
}
@Override
public void configureClientOutboundChannel(ChannelRegistration channelRegistration) {
}
@Override
public boolean configureMessageConverters(List<MessageConverter> arg0) {
return true;
}
additional messages seen on the firefox debug console
Use of getUserData() or setUserData() is deprecated. Use WeakMap or element.dataset instead. requestNotifier.js:64
"Opening Web Socket..." stomp.js:130
Firefox can't establish a connection to the server at wss://127.0.0.5:8888/hello. wsTest:18
"Whoops! Lost connection to wss://127.0.0.5:8888/hello" stomp.js:130
here is full version of the html page
<!DOCTYPE html>
<html>
<head>
<title>Hello WebSocket</title>
<script src="/js/stomp.js"></script>
<script type="text/javascript">
var stompClient = null;
function setConnected(connected) {
document.getElementById('connect').disabled = connected;
document.getElementById('disconnect').disabled = !connected;
document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
document.getElementById('response').innerHTML = '';
}
function connect() {
var socket = new WebSocket("wss://127.0.0.5:8888/hello");
stompClient = Stomp.over(socket);
// stompClient.connect('tito', 'password', function(frame) {
stompClient.connect({}, function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings', function(greeting){
showGreeting(JSON.parse(greeting.body).content);
});
});
}
function disconnect() {
stompClient.disconnect();
setConnected(false);
console.log("Disconnected");
}
function sendName() {
var name = document.getElementById('name').value;
stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name }));
}
function showGreeting(message) {
var response = document.getElementById('response');
var p = document.createElement('p');
p.style.wordWrap = 'break-word';
p.appendChild(document.createTextNode(message));
response.appendChild(p);
}
</script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable
Javascript and reload this page!</h2></noscript>
<div>
<div>
<button id="connect" onclick="connect();">Connect</button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
</div>
<div id="conversationDiv">
<label>What is your name?</label><input type="text" id="name" />
<button id="sendName" onclick="sendName();">Send</button>
<p id="response"></p>
</div>
</div>
</body>
</html>
the stomp script version used is "// Generated by CoffeeScript 1.6.3"
here is how the certificate was generated
$ keytool -genkey -alias tomcat -keyalg RSA -keystore /home/tito/Projects/syncServer/Server/certificate/sync.keystore
Enter keystore password:
Re-enter new password:
What is your first and last name?
[Unknown]: TestFirstName
What is the name of your organizational unit?
[Unknown]: TestOrganizationalUnitName
What is the name of your organization?
[Unknown]: TestOrganization
What is the name of your City or Locality?
[Unknown]: TestCity
What is the name of your State or Province?
[Unknown]: TestState
What is the two-letter country code for this unit?
[Unknown]: BG
Is CN=TestFirstName, OU=TestOrganizationalUnitName, O=TestOrganization, L=TestCity, ST=TestState, C=BG correct?
[no]: yes
Enter key password for <tomcat>
(RETURN if same as keystore password):
Addition: I have also noticed that web sockets are working if i call
https://127.0.0.5:8888/wsTest
but not working if I call
https://localhost:8888/wsTest
however i still have not found why is this happening. This behavior is the same with chrome and firefox.