On Feb 12, 2008 11:58 AM, stephen joseph butler
<[EMAIL PROTECTED]> wrote:
> Hmmm... that's disappointing. But looking at the source, I might be
> able to hack it in myself. Basically, I want to do what is already
> done in HttpConnection#tunnelCreated(). So at least I know it's
> possible!
>
> Thanks for the information.

For anyone wondering how  solved my problem, here's the code I came up
with. In the end, I didn't have to modify any of the HttpClient code,
but my version isn't horribly robust. For instance, it only returns
upgraded sockets (which is fine for IPP), but won't understand an
upgrade request made via a user initiated method. Part of that problem
is that HttpClient discards any responses in the 100 range as being
irrelevant (from what I understand) to the success/failure of the
method. I don't know how 4.x is progressing, but some general way to
hook into the response stream and handle such requests would be nice.

Anyway, I didn't have to solve that problem to make my code work.
Class attached for the curious.
package foobar;

import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import javax.net.SocketFactory;
import javax.net.ssl.HandshakeCompletedEvent;
import javax.net.ssl.HandshakeCompletedListener;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

import org.apache.commons.httpclient.ConnectTimeoutException;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpParser;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.StatusLine;
import org.apache.commons.httpclient.params.HttpConnectionParams;
import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 * <p>Protocol factory to create an encrypted TLS connection over
 * the standard HTTP port. This factory will first create an unecrypted
 * connect, then ask the server for and upgrade to TLS.
 *
 * @since       1.0
 * @author      <a href="mailto:[EMAIL PROTECTED]">Stephen J Butler</a>
 */
public class TLSProtocolSocketFactory implements SecureProtocolSocketFactory
{
        /** Our logger class. */
        private static final Log LOG = LogFactory.getLog( 
TLSProtocolSocketFactory.class );

        /**
         * <p>Callback class to check when SSL callbacks occur. You should call
         * wait on the instance of the callback object.
         *
         * @since       1.0
         * @author      <a href="mailto:[EMAIL PROTECTED]">Stephen J Butler</a>
         */
        protected static class SSLCallback
                implements HandshakeCompletedListener
        {
                /** Our logger class. */
                private static final Log LOG = LogFactory.getLog( 
TLSProtocolSocketFactory.class );

                /**
                 * <p>Construct a simple callback instance.
                 */
                public SSLCallback()
                {
                }

                /**
                 * <p>Handshake has completed. Notify any waiters of this 
object.
                 *
                 * @param       event           Event that completed.
                 */
                public void handshakeCompleted(
                                HandshakeCompletedEvent event
                                )
                {
                        if (LOG.isDebugEnabled())
                        {
                                LOG.debug( "SSL handshake completed." );
                                LOG.debug( "Cipher Suite: " + 
event.getCipherSuite() );
                        }

                        synchronized (this)
                        {
                                notify();
                        }
                }
        }

        /** TLS version 1.0 string. */
        public static final int VERSION_1_0 = 0;

        /** TLS HTTP strings. */
        public static final String[] HTTP_VERSION = new String[] {
                "TLS/1.0"
                };
        public static final String[] SSL_VERSION = new String[] {
                "TLSv1"
                };

        /** New line for headers. */
        private static final char[] CRLF = new char[] {(char) 13, (char) 10};
        /** Instance of our factory. */
        private static final TLSProtocolSocketFactory factory = new 
TLSProtocolSocketFactory();

        /** TLS version we are to use. */
        protected int tlsVersion;

        /**
         * Construct an instance of the protocol factory.
         */
        public TLSProtocolSocketFactory()
        {
                this( VERSION_1_0 );
        }

        /**
         * <p>Construct an instance of the protocol factory.
         *
         * @param       tlsVersion      TLS Version to upgrade to.
         */
        public TLSProtocolSocketFactory(
                        int tlsVersion
                        )
        {
                super();

                this.tlsVersion = tlsVersion;
        }

        /**
         * <p>Get an instance of the factory.
         *
         * @return              Shared instance of the socket factory.
         */
        public static TLSProtocolSocketFactory getSocketFactory()
        {
                return factory;
        }

        /**
         * <p>Attempts to get a new socket connection to the given host, 
binding to any
         * local address and choosing a local port. There is no timeout on the 
connect.
         *
         * @param       host            The host name/IP.
         * @param       port            The port on the host.
         * @return      A connected socket.
         * 
         * @throws      IOException
         *              If an I/O error occurs while creating the socket.
         *
         * @throws      UnknownHostException
         *              If the IP address of the host cannot be determined.

         */
        public Socket createSocket(
                        String host,
                        int port
                        )
                throws IOException,
                       UnknownHostException
        {
                if (LOG.isDebugEnabled())
                        LOG.debug( "Connecting to " + host + ":" + port );

                return tlsUpgrade(
                                SocketFactory.getDefault().createSocket(
                                        host, port
                                        ),
                                host, port
                                );
        }

        /**
         * <p>Attempts to get a new socket connection to the given host with the
         * specified local address and port. There is no timeout on the connect.
         *
         * @param       host            The host name/IP.
         * @param       port            The port on the host.
         * @param       localAddress    The local host name/IP to bind the 
socket to.
         * @param       localPort       The port on the local machine.
         * @return      A connected socket.
         * 
         * @throws      IOException
         *              If an I/O error occurs while creating the socket.
         *
         * @throws      UnknownHostException
         *              If the IP address of the host cannot be determined.
         */

        public Socket createSocket(
                        String host,
                        int port,
                        InetAddress localAddress,
                        int localPort
                        )
                throws IOException,
                       UnknownHostException
        {
                if (LOG.isDebugEnabled())
                        LOG.debug( "Connecting to " + host + ":" + port + " 
from " + localAddress + ":" + localPort );

                return tlsUpgrade(
                                SocketFactory.getDefault().createSocket(
                                        host, port, localAddress, localPort
                                        ),
                                host, port
                                );
        }

        /**
         * <p>Attempts to get a new socket connection to the given host within 
the given time limit.
         *
         * @param       host            The host name/IP.
         * @param       port            The port on the host.
         * @param       localAddress    The local host name/IP to bind the 
socket to.
         * @param       localPort       The port on the local machine.
         * @param       params          [EMAIL PROTECTED] HttpConnectionParams} 
for the new socket.
         * @return      A connected socket.
         * 
         * @throws      IOException
         *              If an I/O error occurs while creating the socket.
         *
         * @throws      UnknownHostException
         *              If the IP address of the host cannot be determined.
         *
         * @throws      ConnectTimeoutException
         *              If socket cannot be connected within the given time 
limit.
         */
        public Socket createSocket(
                        final String host,
                        final int port,
                        final InetAddress localAddress,
                        final int localPort,
                        final HttpConnectionParams params
                        )
                throws IOException,
                       UnknownHostException,
                       ConnectTimeoutException
        {
                int timeout;

                if (params == null)
                        throw new IllegalArgumentException( "Parameters may not 
be null" );

                timeout = params.getConnectionTimeout();
                if (timeout == 0)
                        return createSocket( host, port, localAddress, 
localPort );
                else
                {
                        Socket sock = SocketFactory.getDefault().createSocket();

                        if (LOG.isDebugEnabled())
                                LOG.debug( "Binding to " + localAddress + ":" + 
localPort );
                        sock.bind( new InetSocketAddress( localAddress, 
localPort ) );

                        if (LOG.isDebugEnabled())
                                LOG.debug( "Connecting to " + host + ":" + port 
+ " with timeout " + timeout );
                        sock.connect( new InetSocketAddress( host, port ), 
timeout );

                        return tlsUpgrade( sock, host, port );
                }
        }


        /**
         * <p>Takes an existing socket that is unsecure and issue the TLS 
upgrade commands
         * over it.
         *
         * @param       socket          Socket to upgrade.
         * @param       host            The host name/IP.
         * @param       port            The port on the host.
         * @return      An upgraded, connected socket.
         * 
         * @throws      IOException
         *              If an I/O error occurs while creating the socket.
         *
         * @throws      UnknownHostException
         *              If the IP address of the host cannot be determined.
         */
        public Socket createSocket(
                        Socket socket,
                        String host,
                        int port,
                        boolean autoClose
                        )
                throws IOException,
                       UnknownHostException
        {
                if (LOG.isDebugEnabled())
                        LOG.debug( "Reconnecting socket to " + host + ":" + 
port );

                return tlsUpgrade( socket, host, port );
        }

        /**
         * <p>Instances of TLSProtocolSocketFactory are the same if they 
upgrade to
         * the same TLS version.
         *
         * @return      True if both objects have the same TLS version.
         */
        public boolean equals(
                        Object rhs
                        )
        {
                if (rhs instanceof TLSProtocolSocketFactory)
                {
                        TLSProtocolSocketFactory other = 
(TLSProtocolSocketFactory)rhs;

                        return tlsVersion == other.tlsVersion;
                }
                else
                        return false;
        }

        /**
         * <p>Get the TLS version this protocol factory will use.
         *
         * @return      TLS version.
         */
        public int getTLSVersion()
        {
                return tlsVersion;
        }

        /**
         * <p>Instances of TLSProtocolSocketFactory have the same hash code.
         *
         * @return      Hash code.
         */
        public int hashCode()
        {
                return getClass().hashCode();
        }

        /**
         * <p>Read a TLS response from the server, returning the status code.
         * All we really care about is the status line, so throw the headers
         * away.
         *
         * @param       socket          Socket to read from.
         * @return      HTTP status line.
         */
        protected StatusLine tlsReadResponse(
                        Socket socket
                        )
                throws IOException
        {
                InputStream in = socket.getInputStream();
                String line;
                Header[] headers;
                StatusLine status;

                // while the returned lines don't start with HTTP, read them in
                do
                {
                        line = HttpParser.readLine( in, "US-ASCII" );
                        if (line == null)
                                throw new IOException( "Unexpected null waiting 
for response" );
                } while (!StatusLine.startsWithHTTP( line ));

                // get the status line and all the headers
                status = new StatusLine( line );
                headers = HttpParser.parseHeaders( in, "US-ASCII" );

                return status;
        }

        /**
         * <p>Take an unencrypted connection and force upgrade it to TLS via an
         * HTTP TLS upgrade request. This ammounts to sending the following to
         * the server:
         *
         * <pre>OPTIONS * HTTP/1.1
         * Host: $REMOTE_HOST
         * Upgrade: $TLS_VERSION
         * Connection: Upgrade</pre>
         *
         * <p>A success is indicated by a status return of 101. If the 
connection
         * could not be upgraded, then an IOException is thrown.
         *
         * @param       socket          Socket to upgrade.
         * @param       host            Remote host the socket is connected to.
         * @param       port            Remote port the socket is connected to.
         * @return      A new socket, this time upgraded to TLS.
         *
         * @throws      IOException
         *              If the socket had a problem during the upgrade.
         */
        protected Socket tlsUpgrade(
                        Socket socket,
                        String host,
                        int port
                        )
                throws IOException
        {
                StringBuilder sb = new StringBuilder();
                OutputStream out = socket.getOutputStream();
                SSLSocket sslSocket;
                SSLCallback callback = new SSLCallback();
                StatusLine status;

                // build our options request
                sb.append( "OPTIONS * HTTP/1.1" ).append( CRLF );
                sb.append( "Host: " ).append( host ).append( ":" ).append( port 
).append( CRLF );
                sb.append( "Upgrade: " ).append( HTTP_VERSION[ tlsVersion ] 
).append( CRLF );
                sb.append( "Connection: Upgrade" ).append( CRLF );
                sb.append( CRLF );

                // write it to the socket
                out.write( sb.toString().getBytes( "US-ASCII" ) );

                // check to make sure the request to switch protocols was 
successful
                status = tlsReadResponse( socket );
                if (status.getStatusCode() != HttpStatus.SC_SWITCHING_PROTOCOLS)
                        throw new IOException(
                                        "Unable to upgrade connection to TLS (" 
+
                                        status.toString() + ")"
                                        );

                // and now, create our secure socket
                LOG.debug( "Creating SSL socket" );
                sslSocket = 
(SSLSocket)(((SSLSocketFactory)(SSLSocketFactory.getDefault())).createSocket( 
socket, host, port, true ));
                sslSocket.setEnabledProtocols( new String[] { SSL_VERSION[ 
tlsVersion ] } );
                sslSocket.addHandshakeCompletedListener( callback );
                sslSocket.startHandshake();

                // wait for the callback to be signaled, which will tell us 
when the handshake
                // completed
                try
                {
                        LOG.debug( "Waiting for handshake to complete" );
                        synchronized (callback)
                        {
                                callback.wait();
                        }
                }
                catch (InterruptedException interruptedException)
                {
                        throw new IOException( "Could not complete SSL 
handshake" );
                }

                // finally, read in the original OPTIONS request
                LOG.debug( "Reading original options request" );
                status = tlsReadResponse( sslSocket );
                if ((status.getStatusCode() < 200) || (status.getStatusCode() 
>= 300))
                        throw new IOException(
                                        "Server failed to respond to the 
options request (" +
                                        status.toString() + ")"
                                        );

                LOG.debug( "Socket upgraded to TLS" );
                return sslSocket;
        }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, e-mail: [EMAIL PROTECTED]

Reply via email to