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