SSL Support in CoralReactor through SSLSocketChannel

CoralReactor implements its own non-blocking SSLSocketChannel so you can have out-of-box support for SSL. In this article we show how you can connect to a SSL server (https, wss, etc.) easily with a CoralReactor client.

A Simple HTTP Client

For plain-text HTTP port 80 it is extremely easy. For example, to connect to www.google.com and fetch the HTTP response headers you can write the simple cliente below:

package com.coralblocks.coralreactor.client.ssl;

import static com.coralblocks.corallog.Log.*;

import java.net.URL;
import java.nio.ByteBuffer;

import com.coralblocks.coralbits.util.ByteBufferUtils;
import com.coralblocks.coralreactor.client.AbstractLineTcpClient;
import com.coralblocks.coralreactor.client.Client;
import com.coralblocks.coralreactor.nio.NioReactor;

public class SSLClient extends AbstractLineTcpClient {

	public SSLClient(NioReactor nio, String host, int port) {
		super(nio, host, port);
	}

	@Override
	protected void handleConnectionOpened() {
		send("GET / HTTP/1.0\n");
	}

	@Override
	protected void handleMessage(ByteBuffer msg) {
		// only print the HTTP response headers
		String s = ByteBufferUtils.parseString(msg);
		if (s.startsWith("<")) {
			close();
		} else {
			System.out.println(s);
		}
	}

	public static void main(String[] args) throws Exception {
		
		URL url = new URL("http://www.google.com"); // note we are using HTTP (port 80)
		String proto = url.getProtocol();
		String host = url.getHost();
		int port = url.getDefaultPort();

		NioReactor nio = NioReactor.create();
		
		Info.log("Connecting...", "url=", url, "host=", host, "port=", port, "proto=", proto);
		
		final Client client = new SSLClient(nio, host, port);
		client.open();

		nio.start();
	}
}

And the output:

22:13:40.350783-INFO Connecting... url=http://www.google.com host=www.google.com port=80 proto=http
22:13:40.372449-INFO SSLClient-www.google.com:80 Client opened! sequence=1 session=null
22:13:40.390583-INFO NioReactor Reactor started! type=OptimumNioReactor impl=KQueueSelectorImpl
22:13:40.595306-INFO SSLClient-www.google.com:80 Connection established!
22:13:40.595489-INFO SSLClient-www.google.com:80 Connection opened!
HTTP/1.0 200 OK
Date: Thu, 10 Sep 2015 02:13:40 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=ISO-8859-1
P3P: CP="This is not a P3P policy! See http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657 for more info."
Server: gws
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Set-Cookie: PREF=ID=1111111111111111:FF=0:TM=1441851220:LM=1441851220:V=1:S=XSWC8Y8PJthovQv9; expires=Thu, 31-Dec-2015 16:02:17 GMT; path=/; domain=.google.com
Set-Cookie: NID=71=uGm6eP_jn9OmofaZ4RX10EFlALI8NmfX9jnfSiLrNlWPngHQdR1q_pl2QifvtlJJBmPi6_Dmoacg8pP2A8TgifnQ7EIxQAOoR15DkRohQrBUKn1nFauUkoUwSwgIKgPv; expires=Fri, 11-Mar-2016 02:13:40 GMT; path=/; domain=.google.com; HttpOnly
Accept-Ranges: none
Vary: Accept-Encoding

22:13:40.839168-INFO SSLClient-www.google.com:80 Client was shutdown
22:13:40.839329-INFO SSLClient-www.google.com:80 Client closed!


Switching to HTTPS

SSL in CoralReactor is meant to be plug-and-play: CoralReactor automatically fetches and trusts the server’s certificate, so you don’t have to worry about certificates to make SSL work. All you have to do is turn it on with a config parameter (i.e. useSSL) and pass the appropriate SSL port (i.e. 443) to your client:

	public static void main(String[] args) throws Exception {
		
		URL url = new URL("https://www.google.com"); // note we are using HTTPS now (port 443)
		String proto = url.getProtocol();
		String host = url.getHost();
		int port = url.getDefaultPort();

		NioReactor nio = NioReactor.create();
		MapConfiguration config = new MapConfiguration();
		config.add("useSSL", true); // tell CoralReactor that we want to use SSL
		
		Info.log("Connecting...", "url=", url, "host=", host, "port=", port, "proto=", proto);
		
		final Client client = new SSLClient(nio, host, port, config);
		client.open();

		nio.start();
	}

And the output:

22:51:00.100277-INFO Connecting... url=https://www.google.com host=www.google.com port=443 proto=https
22:51:00.118394-INFO SSLClient-www.google.com:443 Client opened! sequence=1 session=null
22:51:00.880822-INFO NioReactor Reactor started! type=OptimumNioReactor impl=KQueueSelectorImpl
22:51:01.055298-INFO SSLClient-www.google.com:443 Connection established!
22:51:01.055608-INFO SSLClient-www.google.com:443 Connection opened!
HTTP/1.0 200 OK
Date: Thu, 10 Sep 2015 02:51:01 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=ISO-8859-1
P3P: CP="This is not a P3P policy! See http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657 for more info."
Server: gws
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Set-Cookie: PREF=ID=1111111111111111:FF=0:TM=1441853461:LM=1441853461:V=1:S=W287StjqyNrsY-rC; expires=Thu, 31-Dec-2015 16:02:17 GMT; path=/; domain=.google.com
Set-Cookie: NID=71=Z-T634Zo9qGSn9GbdTlmX5KFeU6ZZrzVySqrtfJWuD_nwFbo8Qlsm3EzeRCgiybqBmnW7Mkmn0IdGTVgv6nMaUrX3YtvfsRzQH-FgmJAzGpVv1y9WV2DaLa3UNPgY1uY; expires=Fri, 11-Mar-2016 02:51:01 GMT; path=/; domain=.google.com; HttpOnly
Alternate-Protocol: 443:quic,p=1
Alt-Svc: quic=":443"; p="1"; ma=604800
Accept-Ranges: none
Vary: Accept-Encoding

22:51:01.283551-INFO SSLClient-www.google.com:443 Client was shutdown
22:51:01.283760-INFO SSLClient-www.google.com:443 Client closed!


When is it plug-and-play?

Just like a browser, CoralReactor automatically trusts servers whose certificate is issued by a well-known certificate authority (the ones your Java installation already trusts). That is the case for most servers on the internet, including many exchanges. So if your exchange (or server) gave you only a host and a port, useSSL is all you need.

Two tips:

  • Use the server’s host name (i.e. fix.exchange.com) and not its IP address when you have one. Many servers need the host name during the SSL handshake to present the right certificate, and some refuse the connection without it.
  • If the connection does not work, see Troubleshooting below: CoralReactor will tell you what is wrong.


When it is not plug-and-play: what did your exchange give you?

Some exchanges require extra files, which they send you when you sign up. If they did not send you any file, skip this section. Otherwise, it is one of the two cases below (or both). In both cases you end up with a single keystore file that you pass to CoralReactor with two config parameters. The well-known certificate authorities keep working too: the keystore only adds to them.

1. A CA certificate (a .cer, .crt or .pem file)

The exchange uses its own certificate authority, which Java does not know about, so CoralReactor refuses to trust it until you tell it to. Put the CA certificate in a keystore with keytool (it comes with Java):

$ keytool -importcert -noprompt -alias exchange -file exchange-ca.crt -keystore exchange.jks -storepass mypassword

And tell CoralReactor to use it:

		MapConfiguration config = new MapConfiguration();
		config.add("useSSL", true);
		config.add("sslKeyStoreFile", "/path/to/exchange.jks"); // the keystore you created above
		config.add("sslKeyStorePassword", "mypassword");


2. A client certificate (a .p12 or .pfx file, plus its password)

The exchange wants to identify you with a certificate of your own. Use the file exactly as they sent it, there is nothing to convert or create.

If the exchange also gave you a CA certificate (case 1 above), add it to the same file:

$ keytool -importcert -noprompt -alias exchange -file exchange-ca.crt -keystore client.p12 -storepass thepassword

And tell CoralReactor to use it:

		MapConfiguration config = new MapConfiguration();
		config.add("useSSL", true);
		config.add("sslKeyStoreFile", "/path/to/client.p12"); // the file the exchange sent you
		config.add("sslKeyStorePassword", "thepassword"); // the password the exchange gave you


Connecting through an HTTP proxy

Skip this section if your client can connect to the exchange directly. Use it if your company only lets you reach the exchange through an HTTP proxy.

What you need

Two addresses:

  • The proxy address and port, i.e. proxy.mycompany.com:8080
  • The exchange address and port, i.e. gateway.exchange.com:443

Step 1: connect your client to the proxy

Give your client the proxy address and port, not the exchange’s:

		MapConfiguration config = new MapConfiguration();
		config.add("useSSL", true);
		final Client client = new SSLClient(nio, "proxy.mycompany.com", 8080, config);

Step 2: tell CoralReactor where the exchange is

Add the exchange address and port to the config:

		MapConfiguration config = new MapConfiguration();
		config.add("useSSL", true);
		config.add("sslProxyConnect", "gateway.exchange.com:443"); // or pass through -DsslProxyConnect=gateway.exchange.com:443
		final Client client = new SSLClient(nio, "proxy.mycompany.com", 8080, config);

CoralReactor will ask the proxy to connect to the exchange and then talk SSL with the exchange through it.

Step 3: certificates, only if the exchange gave you files

Nothing changes here compared to a direct connection: if the exchange gave you no files, there is nothing to do. If it gave you a CA certificate or a client certificate, configure it exactly as explained in cases 1 and 2 above.

Step 4: check it works

The first time, start Java with -DsslDebug=true too. When everything is right, the log shows:

Proxy mode: tunnel to gateway.exchange.com:443 is open (HTTP/1.1 200 Connection established)

followed by the SSL handshake with the exchange. If the proxy refuses, the log says so, i.e. The HTTP proxy … refused to connect to gateway.exchange.com:443 (sslProxyConnect), its answer was: HTTP/1.1 403 Forbidden: then ask your network team to allow that address and port on the proxy.

Without sslProxyConnect nothing changes: CoralReactor connects directly, as usual.


Troubleshooting

If the connection does not work, run your client with -DsslDebug=true. CoralReactor will then log every step of the SSL conversation in plain words and, when something fails, what went wrong and what usually causes it. Some examples:

  • the server requires a (valid) client certificate: see case 2 above.
  • the server certificate is not trusted: see case 1 above.
  • the server certificate has expired: the exchange must renew it (also check your machine’s date and time).
  • no SNI (server name indication) was sent: use the host name of the server instead of its IP address.

Failures are logged even without -DsslDebug=true. If you still need help, send us that log. For the lowest level details of the handshake you can also use Java’s own -Djavax.net.debug=ssl:handshake.


Using Stunnel as a SSL Proxy

For a cleaner alternative you can also use stunnel as a SSL proxy. For example to connect to google.com:443 use the stunnel.conf file below:

[remote]
client = yes
accept = 8888
connect = www.google.com:443

Then run stunnel:

$ sudo stunnel stunnel.conf

You can easily test with netcat with the command-line below:

$ cat <(echo -e "GET / HTTP/1.0\n") | nc localhost 8888 | head -n 9
HTTP/1.0 200 OK
Date: Thu, 10 Sep 2015 03:09:13 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=ISO-8859-1
P3P: CP="This is not a P3P policy! See http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657 for more info."
Server: gws
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN