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: [email protected]
For additional commands, e-mail: [email protected]