Bug 66592 - Support for HTTPS proxy in websocket client
Summary: Support for HTTPS proxy in websocket client
Status: REOPENED
Alias: None
Product: Tomcat 9
Classification: Unclassified
Component: Catalina (show other bugs)
Version: 9.0.74
Hardware: All All
: P2 enhancement (vote)
Target Milestone: -----
Assignee: Tomcat Developers Mailing List
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2023-05-04 10:55 UTC by radhika.jaju@veritas.com
Modified: 2023-08-04 12:53 UTC (History)
0 users



Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description radhika.jaju@veritas.com 2023-05-04 10:55:38 UTC
Websocket client library in Apache Tomcat 9.x or all releases is supporting only HTTP proxy server.

WsWebSocketContainer.java has following check:

       Proxy selectedProxy = null;
        for (Proxy proxy : proxies) {
            if (proxy.type().equals(Proxy.Type.HTTP)) {
                sa = proxy.address();
                if (sa instanceof InetSocketAddress) {
                    InetSocketAddress inet = (InetSocketAddress) sa;
                    if (inet.isUnresolved()) {
                        sa = new InetSocketAddress(inet.getHostName(), inet.getPort());
                    }
                }
                selectedProxy = proxy;
                break;
            }
        }

This request is to enhance the clientlibrary to support HTTPS proxy servers.
Comment 1 Mark Thomas 2023-06-02 08:33:02 UTC
HTTPS proxies don't work the way you appear to think they do.

Java's ProxyType.HTTP doesn't mean what you appear to think it means.

Tomcat's WebSocket implementation correctly implements proxying for both ws (clear text / HTTP) and wss (TLS / HTTPS) connections.

If you require further advice on this topic, the Tomcat users list is the place to ask:
https://tomcat.apache.org/lists.html#tomcat-users
Comment 2 radhika.jaju@veritas.com 2023-06-02 13:04:21 UTC
so how should websocket connections be established through a secure proxy:

When a client comes across an https:// URL, it can do one of three things:

  a)  open an TLS connection directly to the origin server, or
  b)  open a tunnel through a proxy to the origin server using the CONNECT request method 
  c)  open an TLS connection to a secure proxy.


For HTTPS Rest API calls i am able to use Apache HTTP Client connect through secure proxy(HTTPS). For this i am using 


  public class MyRoutePlanner implements HttpRoutePlanner {
        @Override
        public HttpRoute determineRoute(HttpHost target, HttpRequest request, HttpContext context)
                throws HttpException {
            return new HttpRoute(target, null, new HttpHost("<secureproxy host>", 8443, "https"), true,
                    TunnelType.PLAIN, LayerType.PLAIN); // Note: true
        }
    }


and plugging this RoutePlanner 

  HttpClientBuilder clientBuilder = HttpClientBuilder.create();
        clientBuilder.setDefaultRequestConfig(config).setConnectionManager(connectionManager)
                .setRoutePlanner(new MyRoutePlanner());

The HTTPS Rest API calls are then routed via the Secure Proxy host. However, i need to set up the Certificate of the secure Proxy server host in the truststore that i use to establish the secure connection to the server. Both target server certificate and secure proxy server certificate are there in the TrustStore.

===================

Similarly, Through the Tomcat websocket client library, the connection to secure proxy is throwing DeploymentException in the function HttpResponse httpResponse = processResponse(response, channel, timeout);

===============
 if (proxyConnect != null) {
                fConnect.get(timeout, TimeUnit.MILLISECONDS);
                // Proxy CONNECT is clear text
                channel = new AsyncChannelWrapperNonSecure(socketChannel);
                writeRequest(channel, proxyConnect, timeout);
                HttpResponse httpResponse = processResponse(response, channel, timeout);
                if (httpResponse.status == Constants.PROXY_AUTHENTICATION_REQUIRED) {
                    return processAuthenticationChallenge(clientEndpointHolder, clientEndpointConfiguration, path,
                            redirectSet, userProperties, request, httpResponse, AuthenticationType.PROXY);
                } else if (httpResponse.getStatus() != 200) {
                    throw new DeploymentException(sm.getString("wsWebSocketContainer.proxyConnectFail", selectedProxy,
                            Integer.toString(httpResponse.getStatus())));
                }
            }
===============

Tomcat's WebSocket implementation correctly implements proxying for both ws (clear text / HTTP) and wss (TLS / HTTPS) connections.
-- This is functioning well for me for HTTP proxy.

The ask is to support wss connections through the Secure Proxy.

FYI, i am setting up a squid proxy server in secure mode through the https_port configuration. check this page from squid: https://wiki.squid-cache.org/Features/HTTPS
Comment 3 Mark Thomas 2023-06-02 16:05:09 UTC
Java's Proxy configuration classes (mainly ProxyType, ProxySelector) don't support proxies that use TLS between the client and the proxy.

Tomcat's proxy support for WebSocket opted to leverage Java's built-in proxy configuration classes rather than re-invent the wheel.

A quick survey of the browsers and operating systems I have available didn't uncover any that allow direct configuration of proxying over TLS via the normal proxy configuration GUI. Those that do support it do so via a PAC file or command line arguments. I wonder if Java's lack of support for proxying over TLS is related.

I couldn't find an enhancement request in the Java bug database for adding support for proxying over TLS.

What is the use case for using proxying over TLS vs just using a direct TLS connection?

If we do implement this (and it is still a big if at this stage in my view) would a custom user property such as "USE_SECURE_PROXY" be sufficient to meet the requirement? The idea being that you'd configure the http/https proxies as normal in Java and then, depending on this flag, Tomcat would connect to the proxy over a clear channel or TLS.
Comment 4 radhika.jaju@veritas.com 2023-06-02 17:13:58 UTC
Hi Mark, Thanks for your response.
Here is some of my understanding and thoughts. 

What is the use case for using proxying over TLS vs just using a direct TLS connection?
>> So some of our Customers (websocket clients) have to connect to the websocket server endpoint hosted in the Cloud. Some of the enterprise customers may engage a HTTPS proxy server in their IT infra which they cannot bypass, the connections have to be established via this Proxy Servers. Such customers are getting blocked in leveraging our services in cloud.

If we do implement this (and it is still a big if at this stage in my view) would a custom user property such as "USE_SECURE_PROXY" be sufficient to meet the requirement? The idea being that you'd configure the http/https proxies as normal in Java and then, depending on this flag, Tomcat would connect to the proxy over a clear channel or TLS.
>> In my understanding, the https_proxy system property in java is meant only to support HTTPS protocol through the proxy server. This is not actually a secure Proxy server. So not sure if it can function as expected. From the websocket client code, i understand that tomcat is only leveraging the java http_proxy/https_proxy configuration only to pass the Proxy server host and port information to the websocket client library, because ultimately a socket connection with the Proxy server host and port gets established and then we send the HTTP "Connect" Request to the Proxy server to create the proxy Tunnel over which https requests/response between client and server gets exchanged. The proxy server is no longer intercepting any of the data flow. These lines in the code.
======
   private static ByteBuffer createProxyRequest(String host, int port, String authorizationHeader) {
        StringBuilder request = new StringBuilder();
        request.append("CONNECT ");
        request.append(host);
        request.append(':');
        request.append(port);

        request.append(" HTTP/1.1\r\nProxy-Connection: keep-alive\r\nConnection: keepalive\r\nHost: ");
        request.append(host);
        request.append(':');
        request.append(port);

        if (authorizationHeader != null) {
            request.append("\r\n");
            request.append(Constants.PROXY_AUTHORIZATION_HEADER_NAME);
            request.append(':');
            request.append(authorizationHeader);
        }

        request.append("\r\n\r\n");

        byte[] bytes = request.toString().getBytes(StandardCharsets.ISO_8859_1);
        return ByteBuffer.wrap(bytes);
    }




       // If sa is null, no proxy is configured so need to create sa
        if (sa == null) {
            sa = new InetSocketAddress(host, port);
        } else {
            proxyConnect = createProxyRequest(host, port,
                    (String) userProperties.get(Constants.PROXY_AUTHORIZATION_HEADER_NAME));
        }
...........
// Proxy CONNECT is clear text
                channel = new AsyncChannelWrapperNonSecure(socketChannel);
                writeRequest(channel, proxyConnect, timeout);
                HttpResponse httpResponse = processResponse(response, channel, timeout);
======

Yes i understand java dosent have any support for a TLS terminating secure proxy server.
I think, beyond setting the https_proxy server, there will be need to have to do a SSL/TLS handshake with secure proxy server in the websocket client library connecting code. the SSLContext to connect to proxy server will be needed too.

config.getUserProperties().put("org.apache.tomcat.websocket.PROXY_SSL_CONTEXT", proxySSLContext);

Secure Proxy Server may not need the CONNECT method call so far from what i understood as there is no Tunnel which gets established due to HTTP CONNECT request. 

I am doubting how the upgrade request for websocket protocol will function when secure proxy server is in between. 

Need some investigation on that aspect, i am not too sure how it functions internally.

Reference from Apache HTTPClient router used during the connection could bring more insight.
Comment 5 radhika.jaju@veritas.com 2023-06-15 12:16:33 UTC
=================
            if (proxyConnect != null) {
                fConnect.get(timeout, TimeUnit.MILLISECONDS);
                // Proxy CONNECT is secure text
                if(secureProxy) {

                	InetSocketAddress inetProxy = (InetSocketAddress)sa;
                    SSLEngine sslEngine = createSSLEngine(clientEndpointConfiguration, inetProxy.getHostName(), inetProxy.getPort());
                    channel = new AsyncChannelWrapperSecure(socketChannel, sslEngine);
                    Future<Void> fHandshake = channel.handshake();
                    fHandshake.get(timeout, TimeUnit.MILLISECONDS);

                } else {
                // Proxy CONNECT is clear text
                	channel = new AsyncChannelWrapperNonSecure(socketChannel);
                }
                writeRequest(channel, proxyConnect, timeout);
                HttpResponse httpResponse = processResponse(response, channel, timeout);
                if (httpResponse.status == Constants.PROXY_AUTHENTICATION_REQUIRED) {
                    return processAuthenticationChallenge(clientEndpointHolder, clientEndpointConfiguration, path,
                            redirectSet, userProperties, request, httpResponse, AuthenticationType.PROXY, container);
                } else if (httpResponse.getStatus() != 200) {
                    throw new DeploymentException("wsWebSocketContainer.proxyConnectFail" +selectedProxy + " "
                            +Integer.toString(httpResponse.getStatus()));
                }
            }

I am able to send Secure CONNECT request to the SSL proxy through this modification in the code to establish secure connection with Proxy.

Receiving HTTPStatus 200, Connection Established reply from the proxy.
=================

However, further ahead, i am not able to make the actual connection to the target server over this tunnel through this code as i am not able to overlay the socket:

=================

           if (secure) {
                // Regardless of whether a non-secure wrapper was created for a
                // proxy CONNECT, need to use TLS from this point on so wrap the
                // original AsynchronousSocketChannel
                SSLEngine sslEngine = createSSLEngine(clientEndpointConfiguration, host, port);
                channel = new AsyncChannelWrapperSecure(socketChannel, sslEngine);
            } else if (channel == null) {
                // Only need to wrap as this point if it wasn't wrapped to process a
                // proxy CONNECT
                channel = new AsyncChannelWrapperNonSecure(socketChannel);
            }

            fConnect.get(timeout, TimeUnit.MILLISECONDS);

            Future<Void> fHandshake = channel.handshake();
            fHandshake.get(timeout, TimeUnit.MILLISECONDS);
=================

The handshake with target server is giving:
java.util.concurrent.ExecutionException: javax.net.ssl.SSLHandshakeException: Invalid Alert message: no sufficient data

If i dont do a SSL handshake with Target server , then i am not able to write the request to the target server.
There is no function to overlay the socketchannel in the AsyncChannelWrapperSecure class.

-----------------------------
However, with a normal socket program, i am able to overlay the secure sockets and able to connect and upgrade to websocket protocol 

Here is the code:

--------------------------

       try {
            // Open the connection
            //Future<Void> fConnect = socketChannel.connect(sa);
        	Socket tunnel = null;
            if (proxyConnect != null) {
                //fConnect.get(timeout, TimeUnit.MILLISECONDS);
                // Proxy CONNECT is clear text
                // channel = new AsyncChannelWrapperNonSecure(socketChannel);
                if (proxySecure) {
                    // Regardless of whether a non-secure wrapper was created for a
                    // proxy CONNECT, need to use TLS from this point on so wrap the
                    // original AsynchronousSocketChannel
               	 	SSLContext sslContext = (SSLContext) userProperties.get(PROXY_SSL_CONTEXT);
               	 	SSLSocketFactory factory = sslContext.getSocketFactory();
               	 	InetSocketAddress proxySA = (InetSocketAddress)sa;
               	 	tunnel =
                         (SSLSocket)factory.createSocket(proxySA.getHostName(), proxySA.getPort());
               	 	
                } else {
                    // Only need to wrap as this point if it wasn't wrapped to
                    // process a
                    // proxy CONNECT
                	SocketFactory factory = SocketFactory.getDefault();
               	 	InetSocketAddress proxySA = (InetSocketAddress)sa;
               	 	tunnel =
                         factory.createSocket(proxySA.getHostName(), proxySA.getPort());
                }
                
                writeRequest(tunnel, proxyConnect, timeout);
                HttpResponse httpResponse = processResponse(response, tunnel, timeout);
                if (httpResponse.status == Constants.PROXY_AUTHENTICATION_REQUIRED) {
                     /*return
                     processAuthenticationChallenge(clientEndpointHolder,
                     clientEndpointConfiguration, path,
                     redirectSet, userProperties, request, httpResponse,
                     AuthenticationType.PROXY);*/  // uncomment the code for proxy auth
                } else if (httpResponse.getStatus() != 200) {
                    throw new DeploymentException("wsWebSocketContainer.proxyConnectFail" + selectedProxy
                            + Integer.toString(httpResponse.getStatus()));
                }
            }
        	
            SSLSocket actualWebSocket = null;
            SSLSocketFactory factory = null;
            if (secure) {
           	 	SSLContext sslContext = (SSLContext) userProperties.get(Constants.SSL_CONTEXT_PROPERTY);
           	 	factory = (SSLSocketFactory)sslContext.getSocketFactory();
           	 	
            } else {
            	factory = (SSLSocketFactory)SSLSocketFactory.getDefault();
            }
            
       	 	if(tunnel != null) {
       	 		actualWebSocket =
                 (SSLSocket)factory.createSocket(tunnel, host, port, true);
       	 	} else {
       	 		actualWebSocket = (SSLSocket)factory.createSocket(host, port);
       	 	}

       	 	
            /*
             * register a callback for handshaking completion event
             */
       	 	actualWebSocket.addHandshakeCompletedListener(
                new HandshakeCompletedListener() {
                    public void handshakeCompleted(
                            HandshakeCompletedEvent event) {
                        System.out.println("Handshake finished!");
                        System.out.println(
                            "\t CipherSuite:" + event.getCipherSuite());
                        System.out.println(
                            "\t SessionId " + event.getSession());
                        System.out.println(
                            "\t PeerHost " + event.getSession().getPeerHost());
                    }
                }
            );

            
       	 writeRequest(actualWebSocket, request, timeout);
       	HttpResponse httpResponse = processResponse(response, actualWebSocket, timeout);
--------------------------

changed writeRequest/processResponse to pass socket as param.

Any clues?
Comment 7 radhika.jaju@veritas.com 2023-08-04 12:53:55 UTC
Hi Mark, Any update on this?