Index: java/org/apache/tomcat/websocket/Constants.java =================================================================== --- java/org/apache/tomcat/websocket/Constants.java (revision 1803605) +++ java/org/apache/tomcat/websocket/Constants.java (working copy) @@ -80,11 +80,20 @@ 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 = @@ -115,6 +124,12 @@ // Configuration for stream behavior static final boolean STREAMS_DROP_EMPTY_MESSAGES = Boolean.getBoolean("org.apache.tomcat.websocket.STREAMS_DROP_EMPTY_MESSAGES"); + + static final boolean REDIRECT_ENABLED = Boolean.getBoolean("org.apache.tomcat.websocket.REDIRECT_ENABLED"); + + //RFC 2616 recommends a maximum of 5 redirections + static final int MAX_REDIRECTIONS = + Integer.getInteger("org.apache.tomcat.websocket.MAX_REDIRECTIONS",5).intValue(); public static final boolean STRICT_SPEC_COMPLIANCE = Boolean.getBoolean( Index: java/org/apache/tomcat/websocket/LocalStrings.properties =================================================================== --- java/org/apache/tomcat/websocket/LocalStrings.properties (revision 1803605) +++ java/org/apache/tomcat/websocket/LocalStrings.properties (working copy) @@ -136,3 +136,6 @@ 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.redirectDisabled=Failed to handle HTTP response code [{0}]. HTTP redirection is disabled +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 Index: java/org/apache/tomcat/websocket/WsWebSocketContainer.java =================================================================== --- java/org/apache/tomcat/websocket/WsWebSocketContainer.java (revision 1803605) +++ java/org/apache/tomcat/websocket/WsWebSocketContainer.java (working copy) @@ -26,6 +26,7 @@ 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 @@ private volatile long defaultMaxSessionIdleTimeout = 0; private int backgroundProcessCount = 0; private int processPeriod = Constants.DEFAULT_PROCESS_PERIOD; + private final Set redirectSet = new HashSet<>(Constants.MAX_REDIRECTIONS); private InstanceManager instanceManager; @@ -340,8 +342,47 @@ writeRequest(channel, request, timeout); HttpResponse httpResponse = processResponse(response, channel, timeout); - // TODO: Handle redirects - if (httpResponse.status != 101) { + + if (httpResponse.status != 101) { + if(isRedirectStatus(httpResponse.status)){ + if (!Constants.REDIRECT_ENABLED) { + throw new DeploymentException(sm.getString("wsWebSocketContainer.redirectDisabled", + Integer.toString(httpResponse.status))); + } + + List 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.add(redirectLocation) || redirectSet.size() > Constants.MAX_REDIRECTIONS) { + throw new DeploymentException(sm.getString("wsWebSocketContainer.redirectThreshold", + redirectLocation, Integer.toString(redirectSet.size()), + Integer.toString(Constants.MAX_REDIRECTIONS))); + } + + return connectToServer(endpoint, clientEndpointConfiguration, redirectLocation); + + } throw new DeploymentException(sm.getString("wsWebSocketContainer.invalidStatus", Integer.toString(httpResponse.status))); } @@ -390,7 +431,7 @@ success = true; } catch (ExecutionException | InterruptedException | SSLException | - EOFException | TimeoutException e) { + EOFException | TimeoutException | URISyntaxException e) { throw new DeploymentException( sm.getString("wsWebSocketContainer.httpRequestFailed"), e); } finally { @@ -446,8 +487,28 @@ toWrite -= thisWrite.intValue(); } } + + + 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 "); Index: test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java =================================================================== --- test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java (revision 1803605) +++ test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java (working copy) @@ -82,6 +82,8 @@ @Test public void testConnectToRootEndpoint() throws Exception { + System.setProperty("org.apache.tomcat.websocket.REDIRECT_ENABLED", "true"); + Tomcat tomcat = getTomcatInstance(); // No file system docBase required Context ctx = tomcat.addContext("", null); @@ -94,14 +96,10 @@ ctx2.addServletMappingDecoded("/", "default"); tomcat.start(); - + 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/"); }