Author: markt Date: Wed Aug 2 18:55:44 2017 New Revision: 1803901 URL: http://svn.apache.org/viewvc?rev=1803901&view=rev Log: Fix https://bz.apache.org/bugzilla/show_bug.cgi?id=57767 Add support to the WebSocket client for following redirects when attempting to establish a WebSocket connection. Patch provided by J Fernandez.
Modified: tomcat/trunk/java/org/apache/tomcat/websocket/Constants.java tomcat/trunk/java/org/apache/tomcat/websocket/LocalStrings.properties tomcat/trunk/java/org/apache/tomcat/websocket/WsWebSocketContainer.java tomcat/trunk/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java tomcat/trunk/webapps/docs/changelog.xml tomcat/trunk/webapps/docs/web-socket-howto.xml Modified: tomcat/trunk/java/org/apache/tomcat/websocket/Constants.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/Constants.java?rev=1803901&r1=1803900&r2=1803901&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/websocket/Constants.java (original) +++ tomcat/trunk/java/org/apache/tomcat/websocket/Constants.java Wed Aug 2 18:55:44 2017 @@ -73,6 +73,13 @@ public class Constants { public static final String IO_TIMEOUT_MS_PROPERTY = "org.apache.tomcat.websocket.IO_TIMEOUT_MS"; public static final long IO_TIMEOUT_MS_DEFAULT = 5000; + + // RFC 2068 recommended a limit of 5 + // Most browsers have a default limit of 20 + public static final String MAX_REDIRECTIONS_PROPERTY = + "org.apache.tomcat.websocket.MAX_REDIRECTIONS"; + public static final int MAX_REDIRECTIONS_DEFAULT = 20; + // HTTP upgrade header names and values public static final String HOST_HEADER_NAME = "Host"; public static final String UPGRADE_HEADER_NAME = "Upgrade"; @@ -80,12 +87,21 @@ public class Constants { public static final String ORIGIN_HEADER_NAME = "Origin"; public static final String CONNECTION_HEADER_NAME = "Connection"; public static final String CONNECTION_HEADER_VALUE = "upgrade"; + public static final String LOCATION_HEADER_NAME = "Location"; 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"; public static final String WS_PROTOCOL_HEADER_NAME = "Sec-WebSocket-Protocol"; public static final String WS_EXTENSIONS_HEADER_NAME = "Sec-WebSocket-Extensions"; + /// HTTP redirection status codes + public static final int MULTIPLE_CHOICES = 300; + public static final int MOVED_PERMANENTLY = 301; + public static final int FOUND = 302; + public static final int SEE_OTHER = 303; + public static final int USE_PROXY = 305; + public static final int TEMPORARY_REDIRECT = 307; + // Configuration for Origin header in client static final String DEFAULT_ORIGIN_HEADER_VALUE = System.getProperty("org.apache.tomcat.websocket.DEFAULT_ORIGIN_HEADER_VALUE"); @@ -117,8 +133,7 @@ public class Constants { Boolean.getBoolean("org.apache.tomcat.websocket.STREAMS_DROP_EMPTY_MESSAGES"); public static final boolean STRICT_SPEC_COMPLIANCE = - Boolean.getBoolean( - "org.apache.tomcat.websocket.STRICT_SPEC_COMPLIANCE"); + Boolean.getBoolean("org.apache.tomcat.websocket.STRICT_SPEC_COMPLIANCE"); public static final List<Extension> INSTALLED_EXTENSIONS; Modified: tomcat/trunk/java/org/apache/tomcat/websocket/LocalStrings.properties URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/LocalStrings.properties?rev=1803901&r1=1803900&r2=1803901&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/websocket/LocalStrings.properties (original) +++ tomcat/trunk/java/org/apache/tomcat/websocket/LocalStrings.properties Wed Aug 2 18:55:44 2017 @@ -136,3 +136,5 @@ wsWebSocketContainer.pathWrongScheme=The wsWebSocketContainer.proxyConnectFail=Failed to connect to the configured Proxy [{0}]. The HTTP response code was [{1}] wsWebSocketContainer.sessionCloseFail=Session with ID [{0}] did not close cleanly wsWebSocketContainer.sslEngineFail=Unable to create SSLEngine to support SSL/TLS connections +wsWebSocketContainer.missingLocationHeader=Failed to handle HTTP response code [{0}]. Missing Location header in response +wsWebSocketContainer.redirectThreshold=Cyclic Location header [{0}] detected / reached max number of redirects [{1}] of max [{2}] \ No newline at end of file Modified: tomcat/trunk/java/org/apache/tomcat/websocket/WsWebSocketContainer.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/WsWebSocketContainer.java?rev=1803901&r1=1803900&r2=1803901&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/websocket/WsWebSocketContainer.java (original) +++ tomcat/trunk/java/org/apache/tomcat/websocket/WsWebSocketContainer.java Wed Aug 2 18:55:44 2017 @@ -26,6 +26,7 @@ import java.net.Proxy; import java.net.ProxySelector; import java.net.SocketAddress; import java.net.URI; +import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousChannelGroup; import java.nio.channels.AsynchronousSocketChannel; @@ -98,6 +99,7 @@ public class WsWebSocketContainer implem private volatile long defaultMaxSessionIdleTimeout = 0; private int backgroundProcessCount = 0; private int processPeriod = Constants.DEFAULT_PROCESS_PERIOD; + private Set<URI> redirectSet = null; private InstanceManager instanceManager; @@ -276,10 +278,11 @@ public class WsWebSocketContainer implem "wsWebSocketContainer.asynchronousSocketChannelFail"), ioe); } + Map<String,Object> userProperties = clientEndpointConfiguration.getUserProperties(); + // Get the connection timeout long timeout = Constants.IO_TIMEOUT_MS_DEFAULT; - String timeoutValue = (String) clientEndpointConfiguration.getUserProperties().get( - Constants.IO_TIMEOUT_MS_PROPERTY); + String timeoutValue = (String) userProperties.get(Constants.IO_TIMEOUT_MS_PROPERTY); if (timeoutValue != null) { timeout = Long.valueOf(timeoutValue).intValue(); } @@ -322,8 +325,7 @@ public class WsWebSocketContainer implem // 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.getUserProperties()); + SSLEngine sslEngine = createSSLEngine(userProperties); channel = new AsyncChannelWrapperSecure(socketChannel, sslEngine); } else if (channel == null) { // Only need to wrap as this point if it wasn't wrapped to process a @@ -340,8 +342,57 @@ public class WsWebSocketContainer implem writeRequest(channel, request, timeout); HttpResponse httpResponse = processResponse(response, channel, timeout); - // TODO: Handle redirects + + // Check maximum permitted redirects + int maxRedirects = Constants.MAX_REDIRECTIONS_DEFAULT; + String maxRedirectsValue = + (String) userProperties.get(Constants.MAX_REDIRECTIONS_PROPERTY); + if (maxRedirectsValue != null) { + maxRedirects = Integer.valueOf(maxRedirectsValue).intValue(); + } + if (httpResponse.status != 101) { + if(isRedirectStatus(httpResponse.status)){ + List<String> locationHeader = + httpResponse.getHandshakeResponse().getHeaders().get( + Constants.LOCATION_HEADER_NAME); + + if (locationHeader == null || locationHeader.isEmpty() || + locationHeader.get(0) == null || locationHeader.get(0).isEmpty()) { + throw new DeploymentException(sm.getString( + "wsWebSocketContainer.missingLocationHeader", + Integer.toString(httpResponse.status))); + } + + URI redirectLocation = URI.create(locationHeader.get(0)).normalize(); + + if (!redirectLocation.isAbsolute()) { + redirectLocation = path.resolve(redirectLocation); + } + + String redirectScheme = redirectLocation.getScheme().toLowerCase(); + + if (redirectScheme.startsWith("http")) { + redirectLocation = new URI(redirectScheme.replace("http", "ws"), + redirectLocation.getUserInfo(), redirectLocation.getHost(), + redirectLocation.getPort(), redirectLocation.getPath(), + redirectLocation.getQuery(), redirectLocation.getFragment()); + } + + if (redirectSet == null) { + redirectSet = new HashSet<>(maxRedirects); + } + + if (!redirectSet.add(redirectLocation) || redirectSet.size() > maxRedirects) { + throw new DeploymentException(sm.getString( + "wsWebSocketContainer.redirectThreshold", redirectLocation, + Integer.toString(redirectSet.size()), + Integer.toString(maxRedirects))); + } + + return connectToServer(endpoint, clientEndpointConfiguration, redirectLocation); + + } throw new DeploymentException(sm.getString("wsWebSocketContainer.invalidStatus", Integer.toString(httpResponse.status))); } @@ -390,7 +441,7 @@ public class WsWebSocketContainer implem success = true; } catch (ExecutionException | InterruptedException | SSLException | - EOFException | TimeoutException e) { + EOFException | TimeoutException | URISyntaxException e) { throw new DeploymentException( sm.getString("wsWebSocketContainer.httpRequestFailed"), e); } finally { @@ -448,6 +499,27 @@ public class WsWebSocketContainer implem } + private static boolean isRedirectStatus(int httpResponseCode) { + + boolean isRedirect = false; + + switch (httpResponseCode) { + case Constants.MULTIPLE_CHOICES: + case Constants.MOVED_PERMANENTLY: + case Constants.FOUND: + case Constants.SEE_OTHER: + case Constants.USE_PROXY: + case Constants.TEMPORARY_REDIRECT: + isRedirect = true; + break; + default: + break; + } + + return isRedirect; + } + + private static ByteBuffer createProxyRequest(String host, int port) { StringBuilder request = new StringBuilder(); request.append("CONNECT "); Modified: tomcat/trunk/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java?rev=1803901&r1=1803900&r2=1803901&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java (original) +++ tomcat/trunk/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java Wed Aug 2 18:55:44 2017 @@ -39,7 +39,6 @@ public class TestWebSocketFrameClient ex @Test public void testConnectToServerEndpoint() throws Exception { - Tomcat tomcat = getTomcatInstance(); // No file system docBase required Context ctx = tomcat.addContext("", null); @@ -81,7 +80,6 @@ public class TestWebSocketFrameClient ex @Test public void testConnectToRootEndpoint() throws Exception { - Tomcat tomcat = getTomcatInstance(); // No file system docBase required Context ctx = tomcat.addContext("", null); @@ -97,25 +95,16 @@ public class TestWebSocketFrameClient ex echoTester(""); echoTester("/"); - // FIXME: The ws client doesn't handle any response other than the upgrade, - // which may or may not be allowed. In that case, the server will return - // a redirect to the root of the webapp to avoid possible broken relative - // paths. - // echoTester("/foo"); + echoTester("/foo"); echoTester("/foo/"); } public void echoTester(String path) throws Exception { - WebSocketContainer wsContainer = - ContainerProvider.getWebSocketContainer(); - ClientEndpointConfig clientEndpointConfig = - ClientEndpointConfig.Builder.create().build(); - Session wsSession = wsContainer.connectToServer( - TesterProgrammaticEndpoint.class, - clientEndpointConfig, - new URI("ws://localhost:" + getPort() + path)); - CountDownLatch latch = - new CountDownLatch(1); + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build(); + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, + clientEndpointConfig, new URI("ws://localhost:" + getPort() + path)); + CountDownLatch latch = new CountDownLatch(1); BasicText handler = new BasicText(latch); wsSession.addMessageHandler(handler); wsSession.getBasicRemote().sendText("Hello"); Modified: tomcat/trunk/webapps/docs/changelog.xml URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1803901&r1=1803900&r2=1803901&view=diff ============================================================================== --- tomcat/trunk/webapps/docs/changelog.xml (original) +++ tomcat/trunk/webapps/docs/changelog.xml Wed Aug 2 18:55:44 2017 @@ -101,6 +101,15 @@ </fix> </changelog> </subsection> + <subsection name="WebSocket"> + <changelog> + <add> + <bug>57767</bug>: Add support to the WebSocket client for following + redirects when attempting to establish a WebSocket connection. Patch + provided by J Fernandez. (markt) + </add> + </changelog> + </subsection> </section> <section name="Tomcat 9.0.0.M25 (markt)" rtext="2017-07-28"> <subsection name="Catalina"> Modified: tomcat/trunk/webapps/docs/web-socket-howto.xml URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/web-socket-howto.xml?rev=1803901&r1=1803900&r2=1803901&view=diff ============================================================================== --- tomcat/trunk/webapps/docs/web-socket-howto.xml (original) +++ tomcat/trunk/webapps/docs/web-socket-howto.xml Wed Aug 2 18:55:44 2017 @@ -114,6 +114,14 @@ set then the <code>org.apache.tomcat.websocket.SSL_TRUSTSTORE</code> and <code>org.apache.tomcat.websocket.SSL_TRUSTSTORE_PWD</code> properties will be ignored.</p> + +<p>When using the WebSocket client to connect to server endpoints, the number of + HTTP redirects that the client will follow is controlled by the + <code>userProperties</code> of the provided + <code>javax.websocket.ClientEndpointConfig</code>. The property is + <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> + </section> </body> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org