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

Reply via email to