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-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org