Implementing Https Tunneling Using Java Secure Socket Extension

If your JSSE application is complaining it can’t tunnel through your proxy, create your own https tunneling socket and use it with the URLConnection APIs.

Summary

The Java Secure Socket Extension (JSSE) library from Sun provides for accessing a secure web server from behind a firewall via proxy tunneling. However, it expects the reply from the proxy to start with “HTTP 1.0” in response to the tunneling request. Otherwise it would throw a IOException. If your proxy does not response accordingly, you need to implement your own proxy tunneling protocol. This article shows you how you can open a SSLSocket that tunnels through the proxy, and use it with the URLConnection APIs to communicate with secure web servers from behind the firewall.(1100 words)

 

By: Pua Yeow Cheong

 

Introduction

The Java Secure Socket Extension (JSSE) library from Sun provides for accessing a secure web server from behind a firewall via proxy tunneling. To do this, the JSSE application needs to set the https.ProxyHost and https.ProxyPort system property. The tunneling code in JSSE checks for “HTTP 1.0” in the response from the proxy. If your proxy, like the one in mine and many other organizations, returns “HTTP 1.1”, you will get a IOException. In this case, you need to implement your own https tunneling protocol. This article shows you how you can create a secure socket that tunnels through the firewall, and pass it to the https stream handler to open https URLs using the URLConnection class.

 

Opening the Http Tunnel Socket to the Proxy

The first step is to open the tunneling socket to the proxy port. The code needed to do this handshaking with the proxy can be found in the sample code SSLClientSocketWithTunneling.java that comes with the JSSE distribution. First, a normal socket is created that connects to the proxy port on the proxy host(Line 65). After the socket has been created, it is passed to the doTunnelHandshake() method where the tunneling protocol with the proxy is called.

 

54              SSLSocketFactory factory =

55                       (SSLSocketFactory)SSLSocketFactory.getDefault();

56             

57              /*

58              * Set up a socket to do tunneling through the proxy.

59              * Start it off as a regular socket, then layer SSL

60              * over the top of it.

61              */

62              tunnelHost = System.getProperty("https.proxyHost");

63              tunnelPort = Integer.getInteger("https.proxyPort").intValue();

64             

65              Socket tunnel = new Socket(tunnelHost, tunnelPort);

66              doTunnelHandshake(tunnel, host, port);

 

In doTunnelHandshake(), a http “CONNECT” command is sent to the proxy, with the hostname of the secure site and port number as the parameters(Line 161). In the original tunneling code on Line 206 in JSSE, it then checks for “HTTP1.0 200” in the reply from the proxy. If your organization’s proxy, like in mine, replies with “HTTP 1.1”, a IOException will be thrown. To get around this, the code here checks for the reply  “200 Connection Established”, which indicates that tunneling is successful(Line 207). You can modify the code to check for the corresponding response you expect from your proxy.

 

……

139       private void doTunnelHandshake(Socket tunnel, String host, int port)

140                                     throws IOException

141       {

142          OutputStream out = tunnel.getOutputStream();

143          String msg = "CONNECT " + host + ":" + port + " HTTP/1.0\n"

144                      + "User-Agent: "

145                      + sun.net.www.protocol.http.HttpURLConnection.userAgent

146                      + "\r\n\r\n";

147          byte b[];

148          try {

149             /*

150             * We really do want ASCII7 -- the http protocol doesn't change

151             * with locale.

152             */

153             b = msg.getBytes("ASCII7");

154          } catch (UnsupportedEncodingException ignored) {

155             /*

156             * If ASCII7 isn't there, something serious is wrong, but

157             * Paranoia Is Good (tm)

158             */

159             b = msg.getBytes();

160          }

161          out.write(b);

162          out.flush();

163         

164          /*

165          * We need to store the reply so we can create a detailed

166          * error message to the user.

167          */

168          byte           reply[] = new byte[200];

169          int            replyLen = 0;

170          int            newlinesSeen = 0;

171          boolean        headerDone = false;  /* Done on first newline */

172         

173          InputStream    in = tunnel.getInputStream();

174          boolean        error = false;

175         

176          while (newlinesSeen < 2) {

177             int i = in.read();

178             if (i < 0) {

179                throw new IOException("Unexpected EOF from proxy");

180             }

181             if (i == '\n') {

182                headerDone = true;

183                ++newlinesSeen;

184             } else if (i != '\r') {

185                newlinesSeen = 0;

186                if (!headerDone && replyLen < reply.length) {

187                   reply[replyLen++] = (byte) i;

188                }

189             }

190          }

191         

192          /*

193          * Converting the byte array to a string is slightly wasteful

194          * in the case where the connection was successful, but it's

195          * insignificant compared to the network overhead.

196          */

197          String replyStr;

198          try {

199             replyStr = new String(reply, 0, replyLen, "ASCII7");

200          } catch (UnsupportedEncodingException ignored) {

201             replyStr = new String(reply, 0, replyLen);

202          }

203         

204          /* We check for Connection Established because our proxy returns

205           * HTTP/1.1 instead of 1.0 */

206          //if (!replyStr.startsWith("HTTP/1.0 200")) {

207          if(replyStr.toLowerCase().indexOf(

208                                        "200 connection established") == -1){

209             throw new IOException("Unable to tunnel through "

210                                  + tunnelHost + ":" + tunnelPort

211                                  + ".  Proxy returns \"" + replyStr + "\"");

212          }

213         

214          /* tunneling Handshake was successful! */

215       }

 

Overlaying the Http Tunnel Socket with the SSL Socket

After the tunneling socket has been created successfully, the next step is to overlay it with the SSL socket. Again, this is not difficult to do:

 

66              doTunnelHandshake(tunnel, host, port);

67             

68              /*

69              * Ok, let's overlay the tunnel socket with SSL.

70              */

71              SSLSocket socket =

72                    (SSLSocket)factory.createSocket(tunnel, host, port, true);

73             

74              /*

75              * register a callback for handshaking completion event

76              */

77              socket.addHandshakeCompletedListener(

78                 new HandshakeCompletedListener() {

79                    public void handshakeCompleted(

80                       HandshakeCompletedEvent event) {

81                       System.out.println("Handshake finished!");

82                       System.out.println(

83                       "\t CipherSuite:" + event.getCipherSuite());

84                       System.out.println(

85                       "\t SessionId " + event.getSession());

86                       System.out.println(

87                       "\t PeerHost " + event.getSession().getPeerHost());

88                    }

89                 }

90              );

 

The code first calls the getDefault() method of the SSLSocketFactory to get an instance of the SSLSocketFactory(Line 54). Next, it passes the tunneling socket that was created in the previous step to the createSocket() method of the SSLSocketFactory. This returns a SSLSocket that is connected to the destination host and port via the proxy tunnel. We can optionally add a HandshakeCompletedListener to the socket if we wish to be informed when the SSL handshaking is completed.

 

The SSLSocket thus created is basically ready for use to transfer secure contents. The startHandshake() method is called to start the SSL handshaking(Line 98). After which, we can issue the http “GET” command to retrieve the secure pages(Line 105):

 

91             

92              /*

93              * send http request

94              *

95              * See SSLSocketClient.java for more information about why

96             * there is a forced handshake here when using PrintWriters.

97              */

98              socket.startHandshake();

99             

100             PrintWriter out = new PrintWriter(

101                                  new BufferedWriter(

102                                     new OutputStreamWriter(

103                                        socket.getOutputStream())));

104            

105             out.println("GET http://www.verisign.com/index.html HTTP/1.0");

106             out.println();

107             out.flush();

 

However, issuing http commands to the tunneling SSL Socket to access web pages is not ideal because it would mean we have to re-write the whole http protocol handler from scratch. Instead, we should use the https URL APIs that is already included in the JSSE for this purpose. To do this, we need to pass the tunneling SSL Socket to the https URL stream handler.

 

Passing the SSL Socket to the Https URL Stream Handler

The JSSE library has a HttpsURLConnection class that is in the com.sun.net.ssl package, which extends the java.net.URLConnection class. A HttpsURLConnection object is returned by the openConnection() method of the URL object when “https” is specified as the protocol. The HttpsURLConnection class has a method, setSSLSocketFactory(), that enables us to set a SSLSocketFactory of our choice. To pass the tunneling SSL Socket to the https URL stream handler, we would set the setSSLSocketFactory() method’s parameter with a socket factory that returns the tunneling SSL Socket that we created in the previous section.

 

To do this, we would wrap the code discussed previously in a SSLTunnelSocketFactory class that extends from the SSLSocketFactory class. The SSLSocketFactory is an abstract class. To extend it, we need to implement the createSocket() methods to return the tunneling SSL Socket that we created previously:

 

12        public SSLTunnelSocketFactory(String proxyhost, String proxyport){

13           tunnelHost = proxyhost;

14           tunnelPort = Integer.parseInt(proxyport);

15           dfactory = (SSLSocketFactory)SSLSocketFactory.getDefault();

16        }

.

.

.

44        public Socket createSocket(Socket s, String host, int port,

45                                   boolean autoClose)

46                                   throws IOException,UnknownHostException

47        {

48       

49           Socket tunnel = new Socket(tunnelHost,tunnelPort);

50          

51           doTunnelHandshake(tunnel,host,port);

52          

53           SSLSocket result = (SSLSocket)dfactory.createSocket(

54                                            tunnel,host,port,autoClose);

55          

56           result.addHandshakeCompletedListener(

57              new HandshakeCompletedListener() {

58              public void handshakeCompleted(HandshakeCompletedEvent event) {

59                 System.out.println("Handshake finished!");

60                 System.out.println(

61                 "\t CipherSuite:" + event.getCipherSuite());

62                 System.out.println(

63                 "\t SessionId " + event.getSession());

64                 System.out.println(

65                 "\t PeerHost " + event.getSession().getPeerHost());

66              }

67              }

68           );

69       

70           result.startHandshake();

71       

72           return result;

73        }

 

Notice that the SSLTunnelSocketFactory contains a default SSL SocketFactory object. The default SSL SocketFactory object can be instantiated from a call to the static method getDefault(). We need this SSL SocketFactory object to overlay the tunnel socket with the SSL Socket as discussed earlier. We also call the getDefaultCipherSuites() and getSupportedCipherSuites() methods of the default SSL SocketFactory object when implementing the corresponding abstract methods of the SSLSocketFactory super class. For implementation details, please refer to the complete source code for the SSLTunnelSocketFactory at the end of this article.

 

Tunneling through the Proxy via URLConnection

To tunnel through the proxy via URLConnection, in our JSSE application, after we call the openConnection() method, we check if the returned object is that of the HttpsURLConnection. If so, we instantiate our SSLTunnelSocketFactory object and set it in the setSSLSocketFactory() method (Line 22 – 25):

 

10     public class URLTunnelReader {

11        private final static String proxyHost = "proxy.sg.ibm.com";

12        private final static String proxyPort = "80";

13       

14        public static void main(String[] args) throws Exception {

15           System.setProperty("java.protocol.handler.pkgs",

16                                       "com.sun.net.ssl.internal.www.protocol");

17           //System.setProperty("https.proxyHost",proxyHost);

18           //System.setProperty("https.proxyPort",proxyPort);

19          

20           URL verisign = new URL("https://www.verisign.com");

21           URLConnection urlc = verisign.openConnection(); //from secure site

22           if(urlc instanceof com.sun.net.ssl.HttpsURLConnection){

23                                   ((com.sun.net.ssl.HttpsURLConnection)urlc).setSSLSocketFactory

24                                  (new SSLTunnelSocketFactory(proxyHost,proxyPort));

25           }

26          

27           BufferedReader in = new BufferedReader(

28                                         new InputStreamReader(

29                                                   urlc.getInputStream()));

30          

31           String inputLine;

32          

33           while ((inputLine = in.readLine()) != null)

34              System.out.println(inputLine);

35          

36           in.close();

37        }

38     }

 

The https URLs can then be accessed using the APIs provided by the URLConnection class. We do not need to worry about the format of the http GET and POST commands, which we would if we use the SSL Socket APIs.

 

The complete source code for the SSLTunnelSocketFactory and the application code that connects to a secure URL using proxy tunneling is included at the end of this article.

 

Conclusion

If your JSSE application could not tunnel through your organization’s firewall, you need to implement your own tunneling socket. The sample code included with the JSSE distribution shows you how to open a SSL socket tunnel. This article goes one step further to show you how you can pass the tunneling socket  to the https URL stream handler, and saves you the trouble of re-writing a http handler.

 

Source code

SSLTunnelSocketFactory.java

URLTunnelReader.java

 

About the author:

Yeow Cheong is a Software Engineer with IBM Emerging Technology Centre in Singapore, creating Java solutions for e-commerce and pervasive computing. His work involves dynamic transcoding of secure web contents into WML for display on WAP devices. The views expressed in this article are the author's and do not represent the view of IBM or any of its affiliated companies. You can contact Yeow Cheong at [email protected]

 

Contact information for editors:

e-mail: [email protected]

snail mail: IBM Emerging Technology Centre, 80 Anson Road, IBM Towers, Singapore 079907.

Hosted by www.Geocities.ws

1