You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2022/09/09 08:46:18 UTC

[tomcat] 06/06: Fix BZ 62312 - add support for forward proxy authentication to WebSocket

This is an automated email from the ASF dual-hosted git repository.

markt pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 69e1c84ea172d0229acc1bacb355c9fc0102f47f
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Fri Sep 9 09:39:24 2022 +0100

    Fix BZ 62312 - add support for forward proxy authentication to WebSocket
    
    https://bz.apache.org/bugzilla/show_bug.cgi?id=62312
    Based on a patch by Joe Mokos
---
 .../tomcat/websocket/AuthenticationType.java       |   8 +-
 java/org/apache/tomcat/websocket/Constants.java    |  10 ++
 .../tomcat/websocket/WsWebSocketContainer.java     |  21 ++-
 .../websocket/TesterWebSocketClientProxy.java      | 192 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |  10 ++
 webapps/docs/web-socket-howto.xml                  |  20 ++-
 6 files changed, 254 insertions(+), 7 deletions(-)

diff --git a/java/org/apache/tomcat/websocket/AuthenticationType.java b/java/org/apache/tomcat/websocket/AuthenticationType.java
index c3a9fa5736..a88ea94358 100644
--- a/java/org/apache/tomcat/websocket/AuthenticationType.java
+++ b/java/org/apache/tomcat/websocket/AuthenticationType.java
@@ -22,7 +22,13 @@ public enum AuthenticationType {
             Constants.WWW_AUTHENTICATE_HEADER_NAME,
             Constants.WS_AUTHENTICATION_USER_NAME,
             Constants.WS_AUTHENTICATION_PASSWORD,
-            Constants.WS_AUTHENTICATION_REALM);
+            Constants.WS_AUTHENTICATION_REALM),
+
+    PROXY(Constants.PROXY_AUTHORIZATION_HEADER_NAME,
+            Constants.PROXY_AUTHENTICATE_HEADER_NAME,
+            Constants.WS_AUTHENTICATION_PROXY_USER_NAME,
+            Constants.WS_AUTHENTICATION_PROXY_PASSWORD,
+            Constants.WS_AUTHENTICATION_PROXY_REALM);
 
     private final String authorizationHeaderName;
     private final String authenticateHeaderName;
diff --git a/java/org/apache/tomcat/websocket/Constants.java b/java/org/apache/tomcat/websocket/Constants.java
index b2a843eba5..c83ab4c431 100644
--- a/java/org/apache/tomcat/websocket/Constants.java
+++ b/java/org/apache/tomcat/websocket/Constants.java
@@ -102,6 +102,8 @@ public class Constants {
     public static final String LOCATION_HEADER_NAME = "Location";
     public static final String AUTHORIZATION_HEADER_NAME = "Authorization";
     public static final String WWW_AUTHENTICATE_HEADER_NAME = "WWW-Authenticate";
+    public static final String PROXY_AUTHORIZATION_HEADER_NAME = "Proxy-Authorization";
+    public static final String PROXY_AUTHENTICATE_HEADER_NAME = "Proxy-Authenticate";
     public static final String WS_VERSION_HEADER_NAME = "Sec-WebSocket-Version";
     public static final String WS_VERSION_HEADER_VALUE = "13";
     public static final String WS_KEY_HEADER_NAME = "Sec-WebSocket-Key";
@@ -116,6 +118,7 @@ public class Constants {
     public static final int USE_PROXY = 305;
     public static final int TEMPORARY_REDIRECT = 307;
     public static final int UNAUTHORIZED = 401;
+    public static final int PROXY_AUTHENTICATION_REQUIRED = 407;
 
     // Configuration for Origin header in client
     static final String DEFAULT_ORIGIN_HEADER_VALUE =
@@ -142,6 +145,13 @@ public class Constants {
     public static final String WS_AUTHENTICATION_PASSWORD = "org.apache.tomcat.websocket.WS_AUTHENTICATION_PASSWORD";
     public static final String WS_AUTHENTICATION_REALM = "org.apache.tomcat.websocket.WS_AUTHENTICATION_REALM";
 
+    public static final String WS_AUTHENTICATION_PROXY_USER_NAME =
+            "org.apache.tomcat.websocket.WS_AUTHENTICATION_PROXY_USER_NAME";
+    public static final String WS_AUTHENTICATION_PROXY_PASSWORD =
+            "org.apache.tomcat.websocket.WS_AUTHENTICATION_PROXY_PASSWORD";
+    public static final String WS_AUTHENTICATION_PROXY_REALM =
+            "org.apache.tomcat.websocket.WS_AUTHENTICATION_PROXY_REALM";
+
     public static final List<Extension> INSTALLED_EXTENSIONS;
 
     static {
diff --git a/java/org/apache/tomcat/websocket/WsWebSocketContainer.java b/java/org/apache/tomcat/websocket/WsWebSocketContainer.java
index 571fe611c9..05bf453eaa 100644
--- a/java/org/apache/tomcat/websocket/WsWebSocketContainer.java
+++ b/java/org/apache/tomcat/websocket/WsWebSocketContainer.java
@@ -250,11 +250,14 @@ public class WsWebSocketContainer implements WebSocketContainer, BackgroundProce
             }
         }
 
+        Map<String,Object> userProperties = clientEndpointConfiguration.getUserProperties();
+
         // 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);
+            proxyConnect = createProxyRequest(
+                    host, port, (String) userProperties.get(Constants.PROXY_AUTHORIZATION_HEADER_NAME));
         }
 
         // Create the initial HTTP request to open the WebSocket connection
@@ -277,8 +280,6 @@ public class WsWebSocketContainer implements WebSocketContainer, BackgroundProce
                     "wsWebSocketContainer.asynchronousSocketChannelFail"), ioe);
         }
 
-        Map<String,Object> userProperties = clientEndpointConfiguration.getUserProperties();
-
         // Get the connection timeout
         long timeout = Constants.IO_TIMEOUT_MS_DEFAULT;
         String timeoutValue = (String) userProperties.get(Constants.IO_TIMEOUT_MS_PROPERTY);
@@ -305,7 +306,10 @@ public class WsWebSocketContainer implements WebSocketContainer, BackgroundProce
                 channel = new AsyncChannelWrapperNonSecure(socketChannel);
                 writeRequest(channel, proxyConnect, timeout);
                 HttpResponse httpResponse = processResponse(response, channel, timeout);
-                if (httpResponse.getStatus() != 200) {
+                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())));
@@ -573,7 +577,7 @@ public class WsWebSocketContainer implements WebSocketContainer, BackgroundProce
     }
 
 
-    private static ByteBuffer createProxyRequest(String host, int port) {
+    private static ByteBuffer createProxyRequest(String host, int port, String authorizationHeader) {
         StringBuilder request = new StringBuilder();
         request.append("CONNECT ");
         request.append(host);
@@ -585,6 +589,13 @@ public class WsWebSocketContainer implements WebSocketContainer, BackgroundProce
         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);
diff --git a/test/org/apache/tomcat/websocket/TesterWebSocketClientProxy.java b/test/org/apache/tomcat/websocket/TesterWebSocketClientProxy.java
new file mode 100644
index 0000000000..89919c8d8a
--- /dev/null
+++ b/test/org/apache/tomcat/websocket/TesterWebSocketClientProxy.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.net.URI;
+import java.util.Queue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.websocket.ClientEndpointConfig;
+import jakarta.websocket.ContainerProvider;
+import jakarta.websocket.Session;
+import jakarta.websocket.WebSocketContainer;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.authenticator.AuthenticatorBase;
+import org.apache.catalina.servlets.DefaultServlet;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.tomcat.util.descriptor.web.LoginConfig;
+import org.apache.tomcat.util.descriptor.web.SecurityCollection;
+import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
+import org.apache.tomcat.websocket.TesterMessageCountClient.BasicText;
+import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint;
+
+/*
+ * Tests WebSocket connections via a forward proxy.
+ *
+ * These tests have been successfully used with Apache Web Server (httpd)
+ * configured with the following:
+ *
+ * Listen 8888
+ * <VirtualHost *:8888>
+ *     ProxyRequests On
+ *     ProxyVia On
+ *     AllowCONNECT 0-65535
+ * </VirtualHost>
+ *
+ * Listen 8889
+ * <VirtualHost *:8889>
+ *     ProxyRequests On
+ *     ProxyVia On
+ *     AllowCONNECT 0-65535
+ *     <Proxy *>
+ *         Order deny,allow
+ *         Allow from all
+ *         AuthType Basic
+ *         AuthName "Proxy Password Required"
+ *         AuthUserFile password.file
+ *         Require valid-user
+ *     </Proxy>
+ * </VirtualHost>
+ *
+ * and
+ * # htpasswd -c password.file proxy
+ * New Password: proxy-pass
+ *
+ */
+public class TesterWebSocketClientProxy extends WebSocketBaseTest {
+
+    private static final String MESSAGE_STRING = "proxy-test-message";
+
+    private static final String PROXY_ADDRESS = "192.168.0.200";
+    private static final String PROXY_PORT_NO_AUTH = "8888";
+    private static final String PROXY_PORT_AUTH = "8889";
+    // The IP address of the test instance that is reachable from the proxy
+    private static final String TOMCAT_ADDRESS = "192.168.0.100";
+
+    private static final String TOMCAT_USER = "tomcat";
+    private static final String TOMCAT_PASSWORD = "tomcat-pass";
+    private static final String TOMCAT_ROLE = "tomcat-role";
+
+    private static final String PROXY_USER = "proxy";
+    private static final String PROXY_PASSWORD = "proxy-pass";
+
+    @Test
+    public void testConnectToServerViaProxyWithNoAuthentication() throws Exception {
+        doTestConnectToServerViaProxy(false, false);
+    }
+
+
+    @Test
+    public void testConnectToServerViaProxyWithServerAuthentication() throws Exception {
+        doTestConnectToServerViaProxy(true, false);
+    }
+
+
+    @Test
+    public void testConnectToServerViaProxyWithProxyAuthentication() throws Exception {
+        doTestConnectToServerViaProxy(false, true);
+    }
+
+
+    @Test
+    public void testConnectToServerViaProxyWithServerAndProxyAuthentication() throws Exception {
+        doTestConnectToServerViaProxy(true, true);
+    }
+
+
+    private void doTestConnectToServerViaProxy(boolean serverAuthentication, boolean proxyAuthentication)
+            throws Exception {
+
+        // Configure the proxy
+        System.setProperty("http.proxyHost", PROXY_ADDRESS);
+        if (proxyAuthentication) {
+            System.setProperty("http.proxyPort", PROXY_PORT_AUTH);
+        } else {
+            System.setProperty("http.proxyPort", PROXY_PORT_NO_AUTH);
+        }
+
+        Tomcat tomcat = getTomcatInstance();
+
+        // Need to listen on all addresses, not just loop-back
+        tomcat.getConnector().setProperty("address", "0.0.0.0");
+
+        // No file system docBase required
+        Context ctx = tomcat.addContext("", null);
+        ctx.addApplicationListener(TesterEchoServer.Config.class.getName());
+        Tomcat.addServlet(ctx, "default", new DefaultServlet());
+        ctx.addServletMappingDecoded("/", "default");
+
+        if (serverAuthentication) {
+            // Configure Realm
+            tomcat.addUser(TOMCAT_USER, TOMCAT_PASSWORD);
+            tomcat.addRole(TOMCAT_USER, TOMCAT_ROLE);
+
+            // Configure security constraints
+            SecurityCollection securityCollection = new SecurityCollection();
+            securityCollection.addPatternDecoded("/*");
+            SecurityConstraint securityConstraint = new SecurityConstraint();
+            securityConstraint.addAuthRole(TOMCAT_ROLE);
+            securityConstraint.addCollection(securityCollection);
+            ctx.addConstraint(securityConstraint);
+
+            // Configure authenticator
+            LoginConfig loginConfig = new LoginConfig();
+            loginConfig.setAuthMethod(BasicAuthenticator.schemeName);
+            ctx.setLoginConfig(loginConfig);
+            AuthenticatorBase basicAuthenticator = new org.apache.catalina.authenticator.BasicAuthenticator();
+            ctx.getPipeline().addValve(basicAuthenticator);
+        }
+
+        tomcat.start();
+
+        WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer();
+
+        ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build();
+        // Configure the client
+        if (serverAuthentication) {
+            clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_USER_NAME, TOMCAT_USER);
+            clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_PASSWORD, TOMCAT_PASSWORD);
+        }
+        if (proxyAuthentication) {
+            clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_PROXY_USER_NAME, PROXY_USER);
+            clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_PROXY_PASSWORD, PROXY_PASSWORD);
+        }
+
+        Session wsSession = wsContainer.connectToServer(
+                TesterProgrammaticEndpoint.class,
+                clientEndpointConfig,
+                new URI("ws://" + TOMCAT_ADDRESS + ":" + getPort() +
+                        TesterEchoServer.Config.PATH_ASYNC));
+        CountDownLatch latch = new CountDownLatch(1);
+        BasicText handler = new BasicText(latch);
+        wsSession.addMessageHandler(handler);
+        wsSession.getBasicRemote().sendText(MESSAGE_STRING);
+
+        boolean latchResult = handler.getLatch().await(10, TimeUnit.SECONDS);
+
+        Assert.assertTrue(latchResult);
+
+        Queue<String> messages = handler.getMessages();
+        Assert.assertEquals(1, messages.size());
+        Assert.assertEquals(MESSAGE_STRING, messages.peek());
+    }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 09aab642b7..5bb482242b 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -282,6 +282,16 @@
       </fix>
     </changelog>
   </subsection>
+  <subsection name="WebSocket">
+    <changelog>
+      <add>
+        <bug>62312</bug>: Add support for authenticating WebSocket clients with
+        an HTTP forward proxy when establishing a connection to a WebSocket
+        endpoint via a foward proxy that requires authentication. Based on a
+        patch provided by Joe Mokos. (markt)
+      </add>
+    </changelog>
+  </subsection>
   <subsection name="Other">
     <changelog>
       <fix>
diff --git a/webapps/docs/web-socket-howto.xml b/webapps/docs/web-socket-howto.xml
index 8e7751d921..20cf2caf38 100644
--- a/webapps/docs/web-socket-howto.xml
+++ b/webapps/docs/web-socket-howto.xml
@@ -141,7 +141,7 @@
    <ocde>org.apache.tomcat.websocket.MAX_REDIRECTIONS</ocde>. The default value
    is 20. Redirection support can be disabled by configuring a value of zero.
    </p>
- 
+
 <p>When using the WebSocket client to connect to a server endpoint that requires
    BASIC or DIGEST authentication, the following user properties must be set:
    </p>
@@ -158,6 +158,24 @@
      <li><code>org.apache.tomcat.websocket.WS_AUTHENTICATION_REALM</code></li>
    </ul>
 
+<p>When using the WebSocket client to connect to a server endpoint via a forward
+   proxy (also known as a gateway) that requires BASIC or DIGEST authentication,
+   the following user properties must be set:
+   </p>
+   <ul>
+     <li><code>org.apache.tomcat.websocket.WS_PROXY_AUTHENTICATION_USER_NAME
+     </code></li>
+     <li><code>org.apache.tomcat.websocket.WS_PROXY_AUTHENTICATION_PASSWORD
+     </code></li>
+   </ul>
+   <p>Optionally, the WebSocket client can be configured only to send
+   credentials if the server authentication challenge includes a specific realm
+   by defining that realm in the optional user property:</p>
+   <ul>
+     <li><code>org.apache.tomcat.websocket.WS_PROXY_AUTHENTICATION_REALM</code>
+     </li>
+   </ul>
+
 </section>
 
 </body>


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org