Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--examples/embedded/src/main/java/org/eclipse/jetty/embedded/WebSocketJsrServer.java1
-rw-r--r--jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrExtension.java20
-rw-r--r--jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/BasicServerEndpointConfigurator.java3
-rw-r--r--jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrCreator.java37
-rw-r--r--jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/ServerContainer.java2
-rw-r--r--jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/ConfiguratorTest.java217
-rw-r--r--jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/WSServer.java1
-rw-r--r--jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/blockhead/BlockheadClient.java744
-rw-r--r--jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/blockhead/BlockheadClientConstructionTest.java71
-rw-r--r--jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/blockhead/HttpResponse.java94
-rw-r--r--jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/blockhead/IncomingFramesCapture.java147
-rw-r--r--jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/browser/JsrBrowserConfigurator.java52
-rw-r--r--jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/browser/JsrBrowserDebugTool.java97
-rw-r--r--jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/browser/JsrBrowserSocket.java227
-rw-r--r--jetty-websocket/javax-websocket-server-impl/src/test/resources/jetty-logging.properties3
-rw-r--r--jetty-websocket/javax-websocket-server-impl/src/test/resources/jsr-browser-debug-tool/index.html37
-rw-r--r--jetty-websocket/javax-websocket-server-impl/src/test/resources/jsr-browser-debug-tool/main.css29
-rw-r--r--jetty-websocket/javax-websocket-server-impl/src/test/resources/jsr-browser-debug-tool/websocket.js128
-rw-r--r--jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/util/QuoteUtil.java29
-rw-r--r--jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketServerFactory.java14
-rw-r--r--jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/blockhead/BlockheadClient.java3
-rw-r--r--jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeResponse.java28
22 files changed, 1976 insertions, 8 deletions
diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/WebSocketJsrServer.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/WebSocketJsrServer.java
index f8fa85a73c..0cc7ad4674 100644
--- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/WebSocketJsrServer.java
+++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/WebSocketJsrServer.java
@@ -24,7 +24,6 @@ import javax.websocket.server.ServerEndpoint;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
-import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.websocket.jsr356.server.ServerContainer;
import org.eclipse.jetty.websocket.jsr356.server.WebSocketConfiguration;
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrExtension.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrExtension.java
index 1c050f6628..633418c314 100644
--- a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrExtension.java
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrExtension.java
@@ -25,6 +25,7 @@ import java.util.Map;
import javax.websocket.Extension;
import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
+import org.eclipse.jetty.websocket.api.util.QuoteUtil;
public class JsrExtension implements Extension
{
@@ -89,4 +90,23 @@ public class JsrExtension implements Extension
{
return parameters;
}
+
+ @Override
+ public String toString()
+ {
+ StringBuilder str = new StringBuilder();
+ str.append(name);
+ for (Parameter param : parameters)
+ {
+ str.append(';');
+ str.append(param.getName());
+ String value = param.getValue();
+ if (value != null)
+ {
+ str.append('=');
+ QuoteUtil.quoteIfNeeded(str,value,";=");
+ }
+ }
+ return str.toString();
+ }
}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/BasicServerEndpointConfigurator.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/BasicServerEndpointConfigurator.java
index d95c46977b..5137303f74 100644
--- a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/BasicServerEndpointConfigurator.java
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/BasicServerEndpointConfigurator.java
@@ -57,8 +57,7 @@ public class BasicServerEndpointConfigurator extends Configurator
@Override
public List<Extension> getNegotiatedExtensions(List<Extension> installed, List<Extension> requested)
{
- /* do nothing */
- return null;
+ return requested;
}
@Override
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrCreator.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrCreator.java
index 0bde7dab94..c4b652adda 100644
--- a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrCreator.java
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrCreator.java
@@ -19,12 +19,18 @@
package org.eclipse.jetty.websocket.jsr356.server;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
+import javax.websocket.Extension;
+import javax.websocket.Extension.Parameter;
import javax.websocket.server.ServerEndpointConfig;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
+import org.eclipse.jetty.websocket.api.extensions.ExtensionFactory;
+import org.eclipse.jetty.websocket.jsr356.JsrExtension;
import org.eclipse.jetty.websocket.jsr356.endpoints.EndpointInstance;
import org.eclipse.jetty.websocket.jsr356.server.pathmap.WebSocketPathSpec;
import org.eclipse.jetty.websocket.server.pathmap.PathSpec;
@@ -36,10 +42,12 @@ public class JsrCreator implements WebSocketCreator
{
private static final Logger LOG = Log.getLogger(JsrCreator.class);
private final ServerEndpointMetadata metadata;
+ private final ExtensionFactory extensionFactory;
- public JsrCreator(ServerEndpointMetadata metadata)
+ public JsrCreator(ServerEndpointMetadata metadata, ExtensionFactory extensionFactory)
{
this.metadata = metadata;
+ this.extensionFactory = extensionFactory;
}
@Override
@@ -78,6 +86,33 @@ public class JsrCreator implements WebSocketCreator
resp.setAcceptedSubProtocol(subprotocol);
}
+ // deal with extensions
+ List<Extension> installedExts = new ArrayList<>();
+ for (String extName : extensionFactory.getAvailableExtensions().keySet())
+ {
+ installedExts.add(new JsrExtension(extName));
+ }
+ List<Extension> requestedExts = new ArrayList<>();
+ for (ExtensionConfig reqCfg : req.getExtensions())
+ {
+ requestedExts.add(new JsrExtension(reqCfg));
+ }
+ List<Extension> usedExts = configurator.getNegotiatedExtensions(installedExts,requestedExts);
+ List<ExtensionConfig> configs = new ArrayList<>();
+ if (usedExts != null)
+ {
+ for (Extension used : usedExts)
+ {
+ ExtensionConfig ecfg = new ExtensionConfig(used.getName());
+ for (Parameter param : used.getParameters())
+ {
+ ecfg.setParameter(param.getName(),param.getValue());
+ }
+ configs.add(ecfg);
+ }
+ }
+ resp.setExtensions(configs);
+
// create endpoint class
try
{
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/ServerContainer.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/ServerContainer.java
index 2bd5c36398..9a18da3dbc 100644
--- a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/ServerContainer.java
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/ServerContainer.java
@@ -80,7 +80,7 @@ public class ServerContainer extends ClientContainer implements javax.websocket.
public void addEndpoint(ServerEndpointMetadata metadata) throws DeploymentException
{
- JsrCreator creator = new JsrCreator(metadata);
+ JsrCreator creator = new JsrCreator(metadata,webSocketServerFactory.getExtensionFactory());
mappedCreator.addMapping(new WebSocketPathSpec(metadata.getPath()),creator);
}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/ConfiguratorTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/ConfiguratorTest.java
new file mode 100644
index 0000000000..dae78b6cde
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/ConfiguratorTest.java
@@ -0,0 +1,217 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import static org.hamcrest.Matchers.*;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import javax.websocket.Extension;
+import javax.websocket.HandshakeResponse;
+import javax.websocket.OnMessage;
+import javax.websocket.Session;
+import javax.websocket.server.HandshakeRequest;
+import javax.websocket.server.ServerEndpoint;
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.util.QuoteUtil;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.eclipse.jetty.websocket.common.frames.TextFrame;
+import org.eclipse.jetty.websocket.jsr356.server.blockhead.BlockheadClient;
+import org.eclipse.jetty.websocket.jsr356.server.blockhead.HttpResponse;
+import org.eclipse.jetty.websocket.jsr356.server.blockhead.IncomingFramesCapture;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class ConfiguratorTest
+{
+ private static final Logger LOG = Log.getLogger(ConfiguratorTest.class);
+
+ public static class EmptyConfigurator extends ServerEndpointConfig.Configurator
+ {
+ }
+
+ @ServerEndpoint(value = "/empty", configurator = EmptyConfigurator.class)
+ public static class EmptySocket
+ {
+ @OnMessage
+ public String echo(String message)
+ {
+ return message;
+ }
+ }
+
+ public static class NoExtensionsConfigurator extends ServerEndpointConfig.Configurator
+ {
+ @Override
+ public List<Extension> getNegotiatedExtensions(List<Extension> installed, List<Extension> requested)
+ {
+ return Collections.emptyList();
+ }
+ }
+
+ @ServerEndpoint(value = "/no-extensions", configurator = NoExtensionsConfigurator.class)
+ public static class NoExtensionsSocket
+ {
+ @OnMessage
+ public String echo(String message)
+ {
+ return message;
+ }
+ }
+
+ public static class CaptureHeadersConfigurator extends ServerEndpointConfig.Configurator
+ {
+ @Override
+ public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response)
+ {
+ super.modifyHandshake(sec,request,response);
+ sec.getUserProperties().put("request-headers",request.getHeaders());
+ }
+ }
+
+ @ServerEndpoint(value = "/capture-request-headers", configurator = CaptureHeadersConfigurator.class)
+ public static class CaptureHeadersSocket
+ {
+ @OnMessage
+ public String getHeaders(Session session, String headerKey)
+ {
+ StringBuilder response = new StringBuilder();
+
+ response.append("Request Header [").append(headerKey).append("]: ");
+ @SuppressWarnings("unchecked")
+ Map<String, List<String>> headers = (Map<String, List<String>>)session.getUserProperties().get("request-headers");
+ if (headers == null)
+ {
+ response.append("<no headers found in session.getUserProperties()>");
+ }
+ else
+ {
+ List<String> values = headers.get(headerKey);
+ if (values == null)
+ {
+ response.append("<header not found>");
+ }
+ else
+ {
+ response.append(QuoteUtil.join(values,","));
+ }
+ }
+
+ return response.toString();
+ }
+ }
+
+ private static Server server;
+ private static URI baseServerUri;
+
+ @BeforeClass
+ public static void startServer() throws Exception
+ {
+ server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(0);
+ server.addConnector(connector);
+
+ ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
+ context.setContextPath("/");
+ server.setHandler(context);
+
+ ServerContainer container = WebSocketConfiguration.configureContext(context);
+ container.addEndpoint(CaptureHeadersSocket.class);
+ container.addEndpoint(EmptySocket.class);
+ container.addEndpoint(NoExtensionsSocket.class);
+
+ server.start();
+ String host = connector.getHost();
+ if (host == null)
+ {
+ host = "localhost";
+ }
+ int port = connector.getLocalPort();
+ baseServerUri = new URI(String.format("ws://%s:%d/",host,port));
+ LOG.debug("Server started on {}",baseServerUri);
+ }
+
+ @AfterClass
+ public static void stopServer() throws Exception
+ {
+ server.stop();
+ }
+
+ @Test
+ public void testEmptyConfigurator() throws Exception
+ {
+ URI uri = baseServerUri.resolve("/empty");
+
+ try (BlockheadClient client = new BlockheadClient(uri))
+ {
+ client.addExtensions("identity");
+ client.connect();
+ client.sendStandardRequest();
+ HttpResponse response = client.readResponseHeader();
+ Assert.assertThat("response.extensions",response.getExtensionsHeader(),is("identity"));
+ }
+ }
+
+ @Test
+ public void testNoExtensionsConfigurator() throws Exception
+ {
+ URI uri = baseServerUri.resolve("/no-extensions");
+
+ try (BlockheadClient client = new BlockheadClient(uri))
+ {
+ client.addExtensions("identity");
+ client.connect();
+ client.sendStandardRequest();
+ HttpResponse response = client.readResponseHeader();
+ Assert.assertThat("response.extensions",response.getExtensionsHeader(),nullValue());
+ }
+ }
+
+ @Test
+ public void testCaptureRequestHeadersConfigurator() throws Exception
+ {
+ URI uri = baseServerUri.resolve("/capture-request-headers");
+
+ try (BlockheadClient client = new BlockheadClient(uri))
+ {
+ client.addHeader("X-Dummy: Bogus\r\n");
+ client.connect();
+ client.sendStandardRequest();
+ client.expectUpgradeResponse();
+
+ client.write(new TextFrame().setPayload("X-Dummy"));
+ IncomingFramesCapture capture = client.readFrames(1,TimeUnit.SECONDS,1);
+ WebSocketFrame frame = capture.getFrames().poll();
+ Assert.assertThat("Frame Response", frame.getPayloadAsUTF8(), is("Request Header [X-Dummy]: \"Bogus\""));
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/WSServer.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/WSServer.java
index d35fe99cab..108099af20 100644
--- a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/WSServer.java
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/WSServer.java
@@ -160,6 +160,7 @@ public class WSServer
{
server = new Server();
ServerConnector connector = new ServerConnector(server);
+ connector.setPort(0);
server.addConnector(connector);
HandlerCollection handlers = new HandlerCollection();
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/blockhead/BlockheadClient.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/blockhead/BlockheadClient.java
new file mode 100644
index 0000000000..13739fb477
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/blockhead/BlockheadClient.java
@@ -0,0 +1,744 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.blockhead;
+
+import static org.hamcrest.Matchers.*;
+
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.SocketTimeoutException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.net.ssl.HttpsURLConnection;
+
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.MappedByteBufferPool;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.api.WriteCallback;
+import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
+import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
+import org.eclipse.jetty.websocket.api.util.WSURI;
+import org.eclipse.jetty.websocket.common.AcceptHash;
+import org.eclipse.jetty.websocket.common.CloseInfo;
+import org.eclipse.jetty.websocket.common.ConnectionState;
+import org.eclipse.jetty.websocket.common.Generator;
+import org.eclipse.jetty.websocket.common.OpCode;
+import org.eclipse.jetty.websocket.common.Parser;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.eclipse.jetty.websocket.common.extensions.ExtensionStack;
+import org.eclipse.jetty.websocket.common.extensions.WebSocketExtensionFactory;
+import org.eclipse.jetty.websocket.common.io.IOState;
+import org.eclipse.jetty.websocket.common.io.IOState.ConnectionStateListener;
+import org.eclipse.jetty.websocket.common.io.http.HttpResponseHeaderParser;
+import org.junit.Assert;
+
+/**
+ * A simple websocket client for performing unit tests with.
+ * <p>
+ * This client will use {@link HttpURLConnection} and {@link HttpsURLConnection} with standard blocking calls to perform websocket requests.
+ * <p>
+ * This client is <u>NOT</u> intended to be performant or follow the websocket spec religiously. In fact, being able to deviate from the websocket spec at will
+ * is desired for this client to operate properly for the unit testing within this module.
+ * <p>
+ * The BlockheadClient should never validate frames or bytes being sent for validity, against any sort of spec, or even sanity. It should, however be honest
+ * with regards to basic IO behavior, a write should work as expected, a read should work as expected, but <u>what</u> byte it sends or reads is not within its
+ * scope.
+ */
+public class BlockheadClient implements IncomingFrames, OutgoingFrames, ConnectionStateListener, Closeable
+{
+ private static final String REQUEST_HASH_KEY = "dGhlIHNhbXBsZSBub25jZQ==";
+ private static final int BUFFER_SIZE = 8192;
+ private static final Logger LOG = Log.getLogger(BlockheadClient.class);
+ /** Set to true to disable timeouts (for debugging reasons) */
+ private boolean debug = false;
+ private final URI destHttpURI;
+ private final URI destWebsocketURI;
+ private final ByteBufferPool bufferPool;
+ private final Generator generator;
+ private final Parser parser;
+ private final IncomingFramesCapture incomingFrames;
+ private final WebSocketExtensionFactory extensionFactory;
+
+ private Socket socket;
+ private OutputStream out;
+ private InputStream in;
+ private int version = 13; // default to RFC-6455
+ private String protocols;
+ private List<String> extensions = new ArrayList<>();
+ private List<String> headers = new ArrayList<>();
+ private byte[] clientmask = new byte[]
+ { (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF };
+ private int timeout = 1000;
+ private AtomicInteger parseCount;
+ private OutgoingFrames outgoing = this;
+ private boolean eof = false;
+ private ExtensionStack extensionStack;
+ private IOState ioState;
+ private CountDownLatch disconnectedLatch = new CountDownLatch(1);
+ private ByteBuffer remainingBuffer;
+ private String connectionValue = "Upgrade";
+
+ public BlockheadClient(URI destWebsocketURI) throws URISyntaxException
+ {
+ this(WebSocketPolicy.newClientPolicy(),destWebsocketURI);
+ }
+
+ public BlockheadClient(WebSocketPolicy policy, URI destWebsocketURI) throws URISyntaxException
+ {
+ Assert.assertThat("Websocket URI scheme",destWebsocketURI.getScheme(),anyOf(is("ws"),is("wss")));
+ this.destWebsocketURI = destWebsocketURI;
+ if (destWebsocketURI.getScheme().equals("wss"))
+ {
+ throw new RuntimeException("Sorry, BlockheadClient does not support SSL");
+ }
+ this.destHttpURI = WSURI.toHttp(destWebsocketURI);
+
+ LOG.debug("WebSocket URI: {}",destWebsocketURI);
+ LOG.debug(" HTTP URI: {}",destHttpURI);
+
+ this.bufferPool = new MappedByteBufferPool(8192);
+ this.generator = new Generator(policy,bufferPool);
+ this.parser = new Parser(policy,bufferPool);
+ this.parseCount = new AtomicInteger(0);
+
+ this.incomingFrames = new IncomingFramesCapture();
+
+ this.extensionFactory = new WebSocketExtensionFactory(policy,bufferPool);
+ this.ioState = new IOState();
+ this.ioState.addListener(this);
+ }
+
+ public void addExtensions(String xtension)
+ {
+ this.extensions.add(xtension);
+ }
+
+ public void addHeader(String header)
+ {
+ this.headers.add(header);
+ }
+
+ public boolean awaitDisconnect(long timeout, TimeUnit unit) throws InterruptedException
+ {
+ return disconnectedLatch.await(timeout,unit);
+ }
+
+ public void clearCaptured()
+ {
+ this.incomingFrames.clear();
+ }
+
+ public void clearExtensions()
+ {
+ extensions.clear();
+ }
+
+ public void close()
+ {
+ LOG.debug("close()");
+ close(-1,null);
+ }
+
+ public void close(int statusCode, String message)
+ {
+ CloseInfo close = new CloseInfo(statusCode,message);
+
+ ioState.onCloseLocal(close);
+
+ if (!ioState.isClosed())
+ {
+ WebSocketFrame frame = close.asFrame();
+ LOG.debug("Issuing: {}",frame);
+ try
+ {
+ write(frame);
+ }
+ catch (IOException e)
+ {
+ LOG.debug(e);
+ }
+ }
+ }
+
+ public void connect() throws IOException
+ {
+ InetAddress destAddr = InetAddress.getByName(destHttpURI.getHost());
+ int port = destHttpURI.getPort();
+
+ SocketAddress endpoint = new InetSocketAddress(destAddr,port);
+
+ socket = new Socket();
+ socket.setSoTimeout(timeout);
+ socket.connect(endpoint);
+
+ out = socket.getOutputStream();
+ in = socket.getInputStream();
+ }
+
+ public void disconnect()
+ {
+ LOG.debug("disconnect");
+ IO.close(in);
+ IO.close(out);
+ disconnectedLatch.countDown();
+ if (socket != null)
+ {
+ try
+ {
+ socket.close();
+ }
+ catch (IOException ignore)
+ {
+ /* ignore */
+ }
+ }
+ }
+
+ public void expectServerDisconnect()
+ {
+ if (eof)
+ {
+ return;
+ }
+
+ try
+ {
+ int len = in.read();
+ if (len == (-1))
+ {
+ // we are disconnected
+ eof = true;
+ return;
+ }
+
+ Assert.assertThat("Expecting no data and proper socket disconnect (issued from server)",len,is(-1));
+ }
+ catch (SocketTimeoutException e)
+ {
+ LOG.warn(e);
+ Assert.fail("Expected a server initiated disconnect, instead the read timed out");
+ }
+ catch (IOException e)
+ {
+ // acceptable path
+ }
+ }
+
+ public HttpResponse expectUpgradeResponse() throws IOException
+ {
+ HttpResponse response = readResponseHeader();
+
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("Response Header: {}{}",'\n',response);
+ }
+
+ Assert.assertThat("Response Status Code",response.getStatusCode(),is(101));
+ Assert.assertThat("Response Status Reason",response.getStatusReason(),is("Switching Protocols"));
+ Assert.assertThat("Response Header[Upgrade]",response.getHeader("Upgrade"),is("WebSocket"));
+ Assert.assertThat("Response Header[Connection]",response.getHeader("Connection"),is("Upgrade"));
+
+ // Validate the Sec-WebSocket-Accept
+ String acceptKey = response.getHeader("Sec-WebSocket-Accept");
+ Assert.assertThat("Response Header[Sec-WebSocket-Accept Exists]",acceptKey,notNullValue());
+
+ String reqKey = REQUEST_HASH_KEY;
+ String expectedHash = AcceptHash.hashKey(reqKey);
+
+ Assert.assertThat("Valid Sec-WebSocket-Accept Hash?",acceptKey,is(expectedHash));
+
+ // collect extensions configured in response header
+ List<ExtensionConfig> configs = getExtensionConfigs(response);
+ extensionStack = new ExtensionStack(this.extensionFactory);
+ extensionStack.negotiate(configs);
+
+ // Start with default routing
+ extensionStack.setNextIncoming(this); // the websocket layer
+ extensionStack.setNextOutgoing(outgoing); // the network layer
+
+ // Configure Parser / Generator
+ extensionStack.configure(parser);
+ extensionStack.configure(generator);
+
+ // Start Stack
+ try
+ {
+ extensionStack.start();
+ }
+ catch (Exception e)
+ {
+ throw new IOException("Unable to start Extension Stack");
+ }
+
+ // configure parser
+ parser.setIncomingFramesHandler(extensionStack);
+ ioState.onOpened();
+
+ LOG.debug("outgoing = {}",outgoing);
+ LOG.debug("incoming = {}",extensionStack);
+
+ return response;
+ }
+
+ public void flush() throws IOException
+ {
+ out.flush();
+ }
+
+ public String getConnectionValue()
+ {
+ return connectionValue;
+ }
+
+ private List<ExtensionConfig> getExtensionConfigs(HttpResponse response)
+ {
+ List<ExtensionConfig> configs = new ArrayList<>();
+
+ String econf = response.getHeader("Sec-WebSocket-Extensions");
+ if (econf != null)
+ {
+ LOG.debug("Found Extension Response: {}",econf);
+ ExtensionConfig config = ExtensionConfig.parse(econf);
+ configs.add(config);
+ }
+ return configs;
+ }
+
+ public List<String> getExtensions()
+ {
+ return extensions;
+ }
+
+ public URI getHttpURI()
+ {
+ return destHttpURI;
+ }
+
+ public IOState getIOState()
+ {
+ return ioState;
+ }
+
+ public String getProtocols()
+ {
+ return protocols;
+ }
+
+ public String getRequestHost()
+ {
+ if (destHttpURI.getPort() > 0)
+ {
+ return String.format("%s:%d",destHttpURI.getHost(),destHttpURI.getPort());
+ }
+ else
+ {
+ return destHttpURI.getHost();
+ }
+ }
+
+ public String getRequestPath()
+ {
+ StringBuilder path = new StringBuilder();
+ path.append(destHttpURI.getPath());
+ if (StringUtil.isNotBlank(destHttpURI.getQuery()))
+ {
+ path.append('?').append(destHttpURI.getQuery());
+ }
+ return path.toString();
+ }
+
+ public String getRequestWebSocketKey()
+ {
+ return REQUEST_HASH_KEY;
+ }
+
+ public String getRequestWebSocketOrigin()
+ {
+ return destWebsocketURI.toASCIIString();
+ }
+
+ public int getVersion()
+ {
+ return version;
+ }
+
+ public URI getWebsocketURI()
+ {
+ return destWebsocketURI;
+ }
+
+ /**
+ * Errors received (after extensions)
+ */
+ @Override
+ public void incomingError(Throwable e)
+ {
+ incomingFrames.incomingError(e);
+ }
+
+ /**
+ * Frames received (after extensions)
+ */
+ @Override
+ public void incomingFrame(Frame frame)
+ {
+ LOG.debug("incoming({})",frame);
+ int count = parseCount.incrementAndGet();
+ if ((count % 10) == 0)
+ {
+ LOG.info("Client parsed {} frames",count);
+ }
+
+ if (frame.getOpCode() == OpCode.CLOSE)
+ {
+ CloseInfo close = new CloseInfo(frame);
+ ioState.onCloseRemote(close);
+ }
+
+ WebSocketFrame copy = WebSocketFrame.copy(frame);
+ incomingFrames.incomingFrame(copy);
+ }
+
+ public boolean isConnected()
+ {
+ return (socket != null) && (socket.isConnected());
+ }
+
+ @Override
+ public void onConnectionStateChange(ConnectionState state)
+ {
+ switch (state)
+ {
+ case CLOSED:
+ // Per Spec, client should not initiate disconnect on its own
+ // this.disconnect();
+ break;
+ case CLOSING:
+ if (ioState.wasRemoteCloseInitiated())
+ {
+ CloseInfo close = ioState.getCloseInfo();
+ close(close.getStatusCode(),close.getReason());
+ }
+ break;
+ default:
+ /* do nothing */
+ break;
+ }
+ }
+
+ @Override
+ public void outgoingFrame(Frame frame, WriteCallback callback)
+ {
+ ByteBuffer headerBuf = generator.generateHeaderBytes(frame);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("writing out: {}",BufferUtil.toDetailString(headerBuf));
+ }
+ try
+ {
+ BufferUtil.writeTo(headerBuf,out);
+ BufferUtil.writeTo(frame.getPayload(),out);
+ out.flush();
+ if (callback != null)
+ {
+ callback.writeSuccess();
+ }
+ }
+ catch (IOException e)
+ {
+ if (callback != null)
+ {
+ callback.writeFailed(e);
+ }
+ }
+ finally
+ {
+ bufferPool.release(headerBuf);
+ }
+
+ if (frame.getOpCode() == OpCode.CLOSE)
+ {
+ disconnect();
+ }
+ }
+
+ public int read(ByteBuffer buf) throws IOException
+ {
+ if (eof)
+ {
+ throw new EOFException("Hit EOF");
+ }
+
+ if ((remainingBuffer != null) && (remainingBuffer.remaining() > 0))
+ {
+ return BufferUtil.put(remainingBuffer,buf);
+ }
+
+ int len = -1;
+ int b;
+ while ((in.available() > 0) && (buf.remaining() > 0))
+ {
+ b = in.read();
+ if (b == (-1))
+ {
+ eof = true;
+ break;
+ }
+ buf.put((byte)b);
+ len++;
+ }
+
+ return len;
+ }
+
+ public IncomingFramesCapture readFrames(int expectedCount, TimeUnit timeoutUnit, int timeoutDuration) throws IOException, TimeoutException
+ {
+ LOG.debug("Read: waiting for {} frame(s) from server",expectedCount);
+
+ ByteBuffer buf = bufferPool.acquire(BUFFER_SIZE,false);
+ BufferUtil.clearToFill(buf);
+ try
+ {
+ long msDur = TimeUnit.MILLISECONDS.convert(timeoutDuration,timeoutUnit);
+ long now = System.currentTimeMillis();
+ long expireOn = now + msDur;
+ LOG.debug("Now: {} - expireOn: {} ({} ms)",now,expireOn,msDur);
+
+ long iter = 0;
+
+ int len = 0;
+ while (incomingFrames.size() < expectedCount)
+ {
+ BufferUtil.clearToFill(buf);
+ len = read(buf);
+ if (len > 0)
+ {
+ BufferUtil.flipToFlush(buf,0);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("Read {} bytes: {}",len,BufferUtil.toDetailString(buf));
+ }
+ parser.parse(buf);
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ {
+ iter++;
+ if ((iter % 10000000) == 0)
+ {
+ LOG.debug("10,000,000 reads of zero length");
+ iter = 0;
+ }
+ }
+ }
+
+ if (!debug && (System.currentTimeMillis() > expireOn))
+ {
+ incomingFrames.dump();
+ throw new TimeoutException(String.format("Timeout reading all %d expected frames. (managed to only read %d frame(s))",expectedCount,
+ incomingFrames.size()));
+ }
+ }
+ }
+ finally
+ {
+ bufferPool.release(buf);
+ }
+
+ return incomingFrames;
+ }
+
+ public HttpResponse readResponseHeader() throws IOException
+ {
+ HttpResponse response = new HttpResponse();
+ HttpResponseHeaderParser parser = new HttpResponseHeaderParser(response);
+
+ ByteBuffer buf = BufferUtil.allocate(512);
+
+ do
+ {
+ BufferUtil.flipToFill(buf);
+ read(buf);
+ BufferUtil.flipToFlush(buf,0);
+ }
+ while (parser.parse(buf) == null);
+
+ remainingBuffer = response.getRemainingBuffer();
+
+ return response;
+ }
+
+ public void sendStandardRequest() throws IOException
+ {
+ StringBuilder req = new StringBuilder();
+ req.append("GET ").append(getRequestPath()).append(" HTTP/1.1\r\n");
+ req.append("Host: ").append(getRequestHost()).append("\r\n");
+ req.append("Upgrade: websocket\r\n");
+ req.append("Connection: ").append(connectionValue).append("\r\n");
+ for (String header : headers)
+ {
+ req.append(header);
+ }
+ req.append("Sec-WebSocket-Key: ").append(getRequestWebSocketKey()).append("\r\n");
+ req.append("Sec-WebSocket-Origin: ").append(getRequestWebSocketOrigin()).append("\r\n");
+ if (StringUtil.isNotBlank(protocols))
+ {
+ req.append("Sec-WebSocket-Protocol: ").append(protocols).append("\r\n");
+ }
+
+ for (String xtension : extensions)
+ {
+ req.append("Sec-WebSocket-Extensions: ").append(xtension).append("\r\n");
+ }
+ req.append("Sec-WebSocket-Version: ").append(version).append("\r\n");
+ req.append("\r\n");
+ writeRaw(req.toString());
+ }
+
+ public void setConnectionValue(String connectionValue)
+ {
+ this.connectionValue = connectionValue;
+ }
+
+ public void setDebug(boolean flag)
+ {
+ this.debug = flag;
+ }
+
+ public void setProtocols(String protocols)
+ {
+ this.protocols = protocols;
+ }
+
+ public void setTimeout(TimeUnit unit, int duration)
+ {
+ this.timeout = (int)TimeUnit.MILLISECONDS.convert(duration,unit);
+ }
+
+ public void setVersion(int version)
+ {
+ this.version = version;
+ }
+
+ public void skipTo(String string) throws IOException
+ {
+ int state = 0;
+
+ while (true)
+ {
+ int b = in.read();
+ if (b < 0)
+ {
+ throw new EOFException();
+ }
+
+ if (b == string.charAt(state))
+ {
+ state++;
+ if (state == string.length())
+ {
+ break;
+ }
+ }
+ else
+ {
+ state = 0;
+ }
+ }
+ }
+
+ public void sleep(TimeUnit unit, int duration) throws InterruptedException
+ {
+ LOG.info("Sleeping for {} {}",duration,unit);
+ unit.sleep(duration);
+ LOG.info("Waking up from sleep");
+ }
+
+ public void write(WebSocketFrame frame) throws IOException
+ {
+ if (!ioState.isOpen())
+ {
+ return;
+ }
+ LOG.debug("write(Frame->{}) to {}",frame,outgoing);
+ if (LOG.isDebugEnabled())
+ {
+ frame.setMask(new byte[]
+ { 0x00, 0x00, 0x00, 0x00 });
+ }
+ else
+ {
+ frame.setMask(clientmask);
+ }
+ extensionStack.outgoingFrame(frame,null);
+ }
+
+ public void writeRaw(ByteBuffer buf) throws IOException
+ {
+ LOG.debug("write(ByteBuffer) {}",BufferUtil.toDetailString(buf));
+ BufferUtil.writeTo(buf,out);
+ }
+
+ public void writeRaw(ByteBuffer buf, int numBytes) throws IOException
+ {
+ int len = Math.min(numBytes,buf.remaining());
+ byte arr[] = new byte[len];
+ buf.get(arr,0,len);
+ out.write(arr);
+ }
+
+ public void writeRaw(String str) throws IOException
+ {
+ LOG.debug("write((String)[{}]){}{})",str.length(),'\n',str);
+ out.write(StringUtil.getBytes(str,StringUtil.__ISO_8859_1));
+ }
+
+ public void writeRawSlowly(ByteBuffer buf, int segmentSize) throws IOException
+ {
+ while (buf.remaining() > 0)
+ {
+ writeRaw(buf,segmentSize);
+ flush();
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/blockhead/BlockheadClientConstructionTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/blockhead/BlockheadClientConstructionTest.java
new file mode 100644
index 0000000000..a88ed7ca68
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/blockhead/BlockheadClientConstructionTest.java
@@ -0,0 +1,71 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.blockhead;
+
+import static org.hamcrest.Matchers.*;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Gotta test some basic constructors of the BlockheadClient.
+ */
+@RunWith(value = Parameterized.class)
+public class BlockheadClientConstructionTest
+{
+ @Parameters
+ public static Collection<Object[]> data()
+ {
+ List<Object[]> data = new ArrayList<>();
+ // @formatter:off
+ data.add(new Object[] { "ws://localhost/", "http://localhost/" });
+ data.add(new Object[] { "ws://localhost:8080/", "http://localhost:8080/" });
+ data.add(new Object[] { "ws://webtide.com/", "http://webtide.com/" });
+ data.add(new Object[] { "ws://www.webtide.com/sockets/chat", "http://www.webtide.com/sockets/chat" });
+ // @formatter:on
+ return data;
+ }
+
+ private URI expectedWsUri;
+ private URI expectedHttpUri;
+
+ public BlockheadClientConstructionTest(String wsuri, String httpuri)
+ {
+ this.expectedWsUri = URI.create(wsuri);
+ this.expectedHttpUri = URI.create(httpuri);
+ }
+
+ @Test
+ public void testURIs() throws URISyntaxException
+ {
+ BlockheadClient client = new BlockheadClient(expectedWsUri);
+ Assert.assertThat("Websocket URI",client.getWebsocketURI(),is(expectedWsUri));
+ Assert.assertThat("Websocket URI",client.getHttpURI(),is(expectedHttpUri));
+ }
+
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/blockhead/HttpResponse.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/blockhead/HttpResponse.java
new file mode 100644
index 0000000000..88f52ebe24
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/blockhead/HttpResponse.java
@@ -0,0 +1,94 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.blockhead;
+
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.eclipse.jetty.websocket.common.io.http.HttpResponseHeaderParseListener;
+
+public class HttpResponse implements HttpResponseHeaderParseListener
+{
+ private int statusCode;
+ private String statusReason;
+ private Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ private ByteBuffer remainingBuffer;
+
+ @Override
+ public void addHeader(String name, String value)
+ {
+ headers.put(name,value);
+ }
+
+ public String getExtensionsHeader()
+ {
+ return getHeader("Sec-WebSocket-Extensions");
+ }
+
+ public String getHeader(String name)
+ {
+ return headers.get(name);
+ }
+
+ public ByteBuffer getRemainingBuffer()
+ {
+ return remainingBuffer;
+ }
+
+ public int getStatusCode()
+ {
+ return statusCode;
+ }
+
+ public String getStatusReason()
+ {
+ return statusReason;
+ }
+
+ @Override
+ public void setRemainingBuffer(ByteBuffer copy)
+ {
+ this.remainingBuffer = copy;
+ }
+
+ @Override
+ public void setStatusCode(int code)
+ {
+ this.statusCode = code;
+ }
+
+ @Override
+ public void setStatusReason(String reason)
+ {
+ this.statusReason = reason;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder str = new StringBuilder();
+ str.append("HTTP/1.1 ").append(statusCode).append(' ').append(statusReason);
+ for (Map.Entry<String, String> entry : headers.entrySet())
+ {
+ str.append('\n').append(entry.getKey()).append(": ").append(entry.getValue());
+ }
+ return str.toString();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/blockhead/IncomingFramesCapture.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/blockhead/IncomingFramesCapture.java
new file mode 100644
index 0000000000..cc7f574b8c
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/blockhead/IncomingFramesCapture.java
@@ -0,0 +1,147 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.blockhead;
+
+import static org.hamcrest.Matchers.*;
+
+import java.util.Queue;
+
+import org.eclipse.jetty.toolchain.test.EventQueue;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.WebSocketException;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
+import org.eclipse.jetty.websocket.common.OpCode;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.junit.Assert;
+
+public class IncomingFramesCapture implements IncomingFrames
+{
+ private static final Logger LOG = Log.getLogger(IncomingFramesCapture.class);
+ private EventQueue<WebSocketFrame> frames = new EventQueue<>();
+ private EventQueue<Throwable> errors = new EventQueue<>();
+
+ public void assertErrorCount(int expectedCount)
+ {
+ Assert.assertThat("Captured error count",errors.size(),is(expectedCount));
+ }
+
+ public void assertFrameCount(int expectedCount)
+ {
+ Assert.assertThat("Captured frame count",frames.size(),is(expectedCount));
+ }
+
+ public void assertHasErrors(Class<? extends WebSocketException> errorType, int expectedCount)
+ {
+ Assert.assertThat(errorType.getSimpleName(),getErrorCount(errorType),is(expectedCount));
+ }
+
+ public void assertHasFrame(byte op)
+ {
+ Assert.assertThat(OpCode.name(op),getFrameCount(op),greaterThanOrEqualTo(1));
+ }
+
+ public void assertHasFrame(byte op, int expectedCount)
+ {
+ Assert.assertThat(OpCode.name(op),getFrameCount(op),is(expectedCount));
+ }
+
+ public void assertHasNoFrames()
+ {
+ Assert.assertThat("Has no frames",frames.size(),is(0));
+ }
+
+ public void assertNoErrors()
+ {
+ Assert.assertThat("Has no errors",errors.size(),is(0));
+ }
+
+ public void clear()
+ {
+ frames.clear();
+ }
+
+ public void dump()
+ {
+ System.err.printf("Captured %d incoming frames%n",frames.size());
+ int i = 0;
+ for (Frame frame : frames)
+ {
+ System.err.printf("[%3d] %s%n",i++,frame);
+ System.err.printf(" payload: %s%n",BufferUtil.toDetailString(frame.getPayload()));
+ }
+ }
+
+ public int getErrorCount(Class<? extends Throwable> errorType)
+ {
+ int count = 0;
+ for (Throwable error : errors)
+ {
+ if (errorType.isInstance(error))
+ {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public Queue<Throwable> getErrors()
+ {
+ return errors;
+ }
+
+ public int getFrameCount(byte op)
+ {
+ int count = 0;
+ for (WebSocketFrame frame : frames)
+ {
+ if (frame.getOpCode() == op)
+ {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public Queue<WebSocketFrame> getFrames()
+ {
+ return frames;
+ }
+
+ @Override
+ public void incomingError(Throwable e)
+ {
+ LOG.debug(e);
+ errors.add(e);
+ }
+
+ @Override
+ public void incomingFrame(Frame frame)
+ {
+ WebSocketFrame copy = WebSocketFrame.copy(frame);
+ frames.add(copy);
+ }
+
+ public int size()
+ {
+ return frames.size();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/browser/JsrBrowserConfigurator.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/browser/JsrBrowserConfigurator.java
new file mode 100644
index 0000000000..0c5be547dc
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/browser/JsrBrowserConfigurator.java
@@ -0,0 +1,52 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.browser;
+
+import java.util.Collections;
+import java.util.List;
+
+import javax.websocket.Extension;
+import javax.websocket.HandshakeResponse;
+import javax.websocket.server.HandshakeRequest;
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.eclipse.jetty.websocket.api.util.QuoteUtil;
+
+public class JsrBrowserConfigurator extends ServerEndpointConfig.Configurator
+{
+ @Override
+ public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response)
+ {
+ super.modifyHandshake(sec,request,response);
+ sec.getUserProperties().put("userAgent",getHeaderValue(request,"User-Agent"));
+ sec.getUserProperties().put("requestedExtensions",getHeaderValue(request,"Sec-WebSocket-Extensions"));
+ }
+
+ private String getHeaderValue(HandshakeRequest request, String key)
+ {
+ List<String> value = request.getHeaders().get("User-Agent");
+ return QuoteUtil.join(value,",");
+ }
+
+ @Override
+ public List<Extension> getNegotiatedExtensions(List<Extension> installed, List<Extension> requested)
+ {
+ return Collections.emptyList();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/browser/JsrBrowserDebugTool.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/browser/JsrBrowserDebugTool.java
new file mode 100644
index 0000000000..ae91e49b77
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/browser/JsrBrowserDebugTool.java
@@ -0,0 +1,97 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.browser;
+
+import javax.websocket.DeploymentException;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.ServerContainer;
+import org.eclipse.jetty.websocket.jsr356.server.WebSocketConfiguration;
+
+/**
+ * Tool to help debug JSR based websocket circumstances reported around browsers.
+ * <p>
+ * Provides a server, with a few simple websocket's that can be twiddled from a browser. This helps with setting up breakpoints and whatnot to help debug our
+ * websocket implementation from the context of a browser client.
+ */
+public class JsrBrowserDebugTool
+{
+ private static final Logger LOG = Log.getLogger(JsrBrowserDebugTool.class);
+
+ public static void main(String[] args)
+ {
+ int port = 8080;
+
+ for (int i = 0; i < args.length; i++)
+ {
+ String a = args[i];
+ if ("-p".equals(a) || "--port".equals(a))
+ {
+ port = Integer.parseInt(args[++i]);
+ }
+ }
+
+ try
+ {
+ JsrBrowserDebugTool tool = new JsrBrowserDebugTool();
+ tool.setupServer(port);
+ tool.runForever();
+ }
+ catch (Throwable t)
+ {
+ LOG.warn(t);
+ }
+ }
+
+ private Server server;
+
+ private void runForever() throws Exception
+ {
+ server.start();
+ server.dumpStdErr();
+ LOG.info("Server available.");
+ server.join();
+ }
+
+ private void setupServer(int port) throws DeploymentException
+ {
+ server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(port);
+ server.addConnector(connector);
+
+ ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
+ context.setContextPath("/");
+ ServletHolder holder = context.addServlet(DefaultServlet.class,"/");
+ holder.setInitParameter("resourceBase","src/test/resources/jsr-browser-debug-tool");
+ holder.setInitParameter("dirAllowed","true");
+ server.setHandler(context);
+
+ ServerContainer container = WebSocketConfiguration.configureContext(context);
+ container.addEndpoint(JsrBrowserSocket.class);
+
+ LOG.info("{} setup on port {}",this.getClass().getName(),port);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/browser/JsrBrowserSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/browser/JsrBrowserSocket.java
new file mode 100644
index 0000000000..0ea19c99fb
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/browser/JsrBrowserSocket.java
@@ -0,0 +1,227 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.browser;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.Random;
+
+import javax.websocket.CloseReason;
+import javax.websocket.OnClose;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.RemoteEndpoint.Async;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+@ServerEndpoint(value = "/", configurator = JsrBrowserConfigurator.class)
+public class JsrBrowserSocket
+{
+ private static class WriteMany implements Runnable
+ {
+ private Async remote;
+ private int size;
+ private int count;
+
+ public WriteMany(Async remote, int size, int count)
+ {
+ this.remote = remote;
+ this.size = size;
+ this.count = count;
+ }
+
+ @Override
+ public void run()
+ {
+ char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-|{}[]():".toCharArray();
+ int lettersLen = letters.length;
+ char randomText[] = new char[size];
+ Random rand = new Random(42);
+ String msg;
+
+ for (int n = 0; n < count; n++)
+ {
+ // create random text
+ for (int i = 0; i < size; i++)
+ {
+ randomText[i] = letters[rand.nextInt(lettersLen)];
+ }
+ msg = String.format("ManyThreads [%s]",String.valueOf(randomText));
+ remote.sendText(msg);
+ }
+ }
+ }
+
+ private static final Logger LOG = Log.getLogger(JsrBrowserSocket.class);
+ private Session session;
+ private Async remote;
+ private String userAgent;
+ private String requestedExtensions;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ LOG.info("Open: {}",session);
+ this.session = session;
+ this.remote = session.getAsyncRemote();
+ this.userAgent = (String)session.getUserProperties().get("userAgent");
+ this.requestedExtensions = (String)session.getUserProperties().get("requestedExtensions");
+ }
+
+ @OnClose
+ public void onClose(CloseReason close)
+ {
+ LOG.info("Close: {}: {}",close.getCloseCode(),close.getReasonPhrase());
+ this.session = null;
+ }
+
+ @OnMessage
+ public void onMessage(String message)
+ {
+ LOG.info("onTextMessage({})",message);
+
+ int idx = message.indexOf(':');
+ if (idx > 0)
+ {
+ String key = message.substring(0,idx).toLowerCase(Locale.ENGLISH);
+ String val = message.substring(idx + 1);
+ switch (key)
+ {
+ case "info":
+ {
+ writeMessage("Using javax.websocket");
+ if (StringUtil.isBlank(userAgent))
+ {
+ writeMessage("Client has no User-Agent");
+ }
+ else
+ {
+ writeMessage("Client User-Agent: " + this.userAgent);
+ }
+
+ if (StringUtil.isBlank(requestedExtensions))
+ {
+ writeMessage("Client requested no Sec-WebSocket-Extensions");
+ }
+ else
+ {
+ writeMessage("Client Sec-WebSocket-Extensions: " + this.requestedExtensions);
+ }
+ break;
+ }
+ case "many":
+ {
+ String parts[] = val.split(",");
+ int size = Integer.parseInt(parts[0]);
+ int count = Integer.parseInt(parts[1]);
+
+ writeManyAsync(size,count);
+ break;
+ }
+ case "manythreads":
+ {
+ String parts[] = val.split(",");
+ int threadCount = Integer.parseInt(parts[0]);
+ int size = Integer.parseInt(parts[1]);
+ int count = Integer.parseInt(parts[2]);
+
+ Thread threads[] = new Thread[threadCount];
+
+ // Setup threads
+ for (int n = 0; n < threadCount; n++)
+ {
+ threads[n] = new Thread(new WriteMany(remote,size,count),"WriteMany[" + n + "]");
+ }
+
+ // Execute threads
+ for (Thread thread : threads)
+ {
+ thread.start();
+ }
+
+ // Drop out of this thread
+ break;
+ }
+ case "time":
+ {
+ Calendar now = Calendar.getInstance();
+ DateFormat sdf = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.FULL,SimpleDateFormat.FULL);
+ writeMessage("Server time: %s",sdf.format(now.getTime()));
+ break;
+ }
+ default:
+ {
+ writeMessage("key[%s] val[%s]",key,val);
+ }
+ }
+ }
+ else
+ {
+ // Not parameterized, echo it back
+ writeMessage(message);
+ }
+ }
+
+ private void writeManyAsync(int size, int count)
+ {
+ char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-|{}[]():".toCharArray();
+ int lettersLen = letters.length;
+ char randomText[] = new char[size];
+ Random rand = new Random(42);
+
+ for (int n = 0; n < count; n++)
+ {
+ // create random text
+ for (int i = 0; i < size; i++)
+ {
+ randomText[i] = letters[rand.nextInt(lettersLen)];
+ }
+ writeMessage("Many [%s]",String.valueOf(randomText));
+ }
+ }
+
+ private void writeMessage(String message)
+ {
+ if (this.session == null)
+ {
+ LOG.debug("Not connected");
+ return;
+ }
+
+ if (session.isOpen() == false)
+ {
+ LOG.debug("Not open");
+ return;
+ }
+
+ // Async write
+ remote.sendText(message);
+ }
+
+ private void writeMessage(String format, Object... args)
+ {
+ writeMessage(String.format(format,args));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/resources/jetty-logging.properties b/jetty-websocket/javax-websocket-server-impl/src/test/resources/jetty-logging.properties
index 88b96eead1..c5a50f6239 100644
--- a/jetty-websocket/javax-websocket-server-impl/src/test/resources/jetty-logging.properties
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/resources/jetty-logging.properties
@@ -6,3 +6,6 @@ org.eclipse.jetty.LEVEL=WARN
# org.eclipse.jetty.websocket.LEVEL=WARN
# org.eclipse.jetty.websocket.common.io.LEVEL=DEBUG
+### Show state changes on BrowserDebugTool
+# -- LEAVE THIS AT DEBUG LEVEL --
+org.eclipse.jetty.websocket.jsr356.server.browser.LEVEL=DEBUG
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/resources/jsr-browser-debug-tool/index.html b/jetty-websocket/javax-websocket-server-impl/src/test/resources/jsr-browser-debug-tool/index.html
new file mode 100644
index 0000000000..ee9ef00de3
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/resources/jsr-browser-debug-tool/index.html
@@ -0,0 +1,37 @@
+<html>
+ <head>
+ <title>Jetty WebSocket Browser -&gt; Server Debug Tool</title>
+ <script type="text/javascript" src="websocket.js"></script>
+ <link rel="stylesheet" type="text/css" href="main.css" media="all" >
+ </head>
+ <body>
+ jetty websocket/browser/javascript -&gt; server debug tool #console
+ <div id="console"></div>
+ <div id="buttons">
+ <input id="connect" class="button" type="submit" name="connect" value="connect"/>
+ <input id="close" class="button" type="submit" name="close" value="close" disabled="disabled"/>
+ <input id="info" class="button" type="submit" name="info" value="info" disabled="disabled"/>
+ <input id="time" class="button" type="submit" name="time" value="time" disabled="disabled"/>
+ <input id="many" class="button" type="submit" name="many" value="many" disabled="disabled"/>
+ <input id="manythreads" class="button" type="submit" name="many" value="manythreads" disabled="disabled"/>
+ <input id="hello" class="button" type="submit" name="hello" value="hello" disabled="disabled"/>
+ <input id="there" class="button" type="submit" name="there" value="there" disabled="disabled"/>
+ <input id="json" class="button" type="submit" name="json" value="json" disabled="disabled"/>
+ </div>
+ <script type="text/javascript">
+ $("connect").onclick = function(event) { wstool.connect(); return false; }
+ $("close").onclick = function(event) {wstool.close(); return false; }
+ $("info").onclick = function(event) {wstool.write("info:"); return false; }
+ $("time").onclick = function(event) {wstool.write("time:"); return false; }
+ $("many").onclick = function(event) {wstool.write("many:15,300"); return false; }
+ $("manythreads").onclick = function(event) {wstool.write("manythreads:20,25,60"); return false; }
+ $("hello").onclick = function(event) {wstool.write("Hello"); return false; }
+ $("there").onclick = function(event) {wstool.write("There"); return false; }
+ $("json").onclick = function(event) {wstool.write("[{\"channel\":\"/meta/subscribe\",\"subscription\":\"/chat/demo\",\"id\":\"2\",\"clientId\":\"81dwnxwbgs0h0bq8968b0a0gyl\",\"timestamp\":\"Thu,"
+ + " 12 Sep 2013 19:42:30 GMT\"},{\"channel\":\"/meta/subscribe\",\"subscription\":\"/members/demo\",\"id\":\"3\",\"clientId\":\"81dwnxwbgs0h0bq8968b0a0gyl\",\"timestamp\":\"Thu,"
+ + " 12 Sep 2013 19:42:30 GMT\"},{\"channel\":\"/chat/demo\",\"data\":{\"user\":\"ch\",\"membership\":\"join\",\"chat\":\"ch"
+ + " has joined\"},\"id\":\"4\",\"clientId\":\"81dwnxwbgs0h0bq8968b0a0gyl\",\"timestamp\":\"Thu,"
+ + " 12 Sep 2013 19:42:30 GMT\"}]"); return false; }
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/resources/jsr-browser-debug-tool/main.css b/jetty-websocket/javax-websocket-server-impl/src/test/resources/jsr-browser-debug-tool/main.css
new file mode 100644
index 0000000000..9eebead468
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/resources/jsr-browser-debug-tool/main.css
@@ -0,0 +1,29 @@
+body {
+ font-family: sans-serif;
+}
+
+div {
+ border: 0px solid black;
+}
+
+div#console {
+ clear: both;
+ width: 40em;
+ height: 20em;
+ overflow: auto;
+ background-color: #f0f0f0;
+ padding: 4px;
+ border: 1px solid black;
+}
+
+div#console .info {
+ color: black;
+}
+
+div#console .client {
+ color: blue;
+}
+
+div#console .server {
+ color: magenta;
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/resources/jsr-browser-debug-tool/websocket.js b/jetty-websocket/javax-websocket-server-impl/src/test/resources/jsr-browser-debug-tool/websocket.js
new file mode 100644
index 0000000000..1bc5fe7894
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/resources/jsr-browser-debug-tool/websocket.js
@@ -0,0 +1,128 @@
+if (!window.WebSocket && window.MozWebSocket) {
+ window.WebSocket = window.MozWebSocket;
+}
+
+if (!window.WebSocket) {
+ alert("WebSocket not supported by this browser");
+}
+
+function $() {
+ return document.getElementById(arguments[0]);
+}
+function $F() {
+ return document.getElementById(arguments[0]).value;
+}
+
+function getKeyCode(ev) {
+ if (window.event)
+ return window.event.keyCode;
+ return ev.keyCode;
+}
+
+var wstool = {
+ connect : function() {
+ var location = document.location.toString().replace('http://', 'ws://')
+ .replace('https://', 'wss://');
+
+ wstool.info("Document URI: " + document.location);
+ wstool.info("WS URI: " + location);
+
+ this._scount = 0;
+
+ try {
+ this._ws = new WebSocket(location, "tool");
+ this._ws.onopen = this._onopen;
+ this._ws.onmessage = this._onmessage;
+ this._ws.onclose = this._onclose;
+ } catch (exception) {
+ wstool.info("Connect Error: " + exception);
+ }
+ },
+
+ close : function() {
+ this._ws.close();
+ },
+
+ _out : function(css, message) {
+ var console = $('console');
+ var spanText = document.createElement('span');
+ spanText.className = 'text ' + css;
+ spanText.innerHTML = message;
+ var lineBreak = document.createElement('br');
+ console.appendChild(spanText);
+ console.appendChild(lineBreak);
+ console.scrollTop = console.scrollHeight - console.clientHeight;
+ },
+
+ info : function(message) {
+ wstool._out("info", message);
+ },
+
+ infoc : function(message) {
+ wstool._out("client", "[c] " + message);
+ },
+
+ infos : function(message) {
+ this._scount++;
+ wstool._out("server", "[s" + this._scount + "] " + message);
+ },
+
+ setState : function(enabled) {
+ $('connect').disabled = enabled;
+ $('close').disabled = !enabled;
+ $('info').disabled = !enabled;
+ $('time').disabled = !enabled;
+ $('many').disabled = !enabled;
+ $('manythreads').disabled = !enabled;
+ $('hello').disabled = !enabled;
+ $('there').disabled = !enabled;
+ $('json').disabled = !enabled;
+ },
+
+ _onopen : function() {
+ wstool.setState(true);
+ wstool.info("Websocket Connected");
+ },
+
+ _send : function(message) {
+ if (this._ws) {
+ this._ws.send(message);
+ wstool.infoc(message);
+ }
+ },
+
+ write : function(text) {
+ wstool._send(text);
+ },
+
+ _onmessage : function(m) {
+ if (m.data) {
+ wstool.infos(m.data);
+ }
+ },
+
+ _onclose : function(closeEvent) {
+ this._ws = null;
+ wstool.setState(false);
+ wstool.info("Websocket Closed");
+ wstool.info(" .wasClean = " + closeEvent.wasClean);
+
+ var codeMap = {};
+ codeMap[1000] = "(NORMAL)";
+ codeMap[1001] = "(ENDPOINT_GOING_AWAY)";
+ codeMap[1002] = "(PROTOCOL_ERROR)";
+ codeMap[1003] = "(UNSUPPORTED_DATA)";
+ codeMap[1004] = "(UNUSED/RESERVED)";
+ codeMap[1005] = "(INTERNAL/NO_CODE_PRESENT)";
+ codeMap[1006] = "(INTERNAL/ABNORMAL_CLOSE)";
+ codeMap[1007] = "(BAD_DATA)";
+ codeMap[1008] = "(POLICY_VIOLATION)";
+ codeMap[1009] = "(MESSAGE_TOO_BIG)";
+ codeMap[1010] = "(HANDSHAKE/EXT_FAILURE)";
+ codeMap[1011] = "(SERVER/UNEXPECTED_CONDITION)";
+ codeMap[1015] = "(INTERNAL/TLS_ERROR)";
+ var codeStr = codeMap[closeEvent.code];
+ wstool.info(" .code = " + closeEvent.code + " " + codeStr);
+ wstool.info(" .reason = " + closeEvent.reason);
+ }
+};
diff --git a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/util/QuoteUtil.java b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/util/QuoteUtil.java
index 2e17cef9b3..eed6f20ed1 100644
--- a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/util/QuoteUtil.java
+++ b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/util/QuoteUtil.java
@@ -18,8 +18,8 @@
package org.eclipse.jetty.websocket.api.util;
-import java.io.IOException;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;
@@ -473,4 +473,31 @@ public class QuoteUtil
}
return ret.toString();
}
+
+ public static String join(Collection<?> objs, String delim)
+ {
+ if (objs == null)
+ {
+ return "";
+ }
+ StringBuilder ret = new StringBuilder();
+ boolean needDelim = false;
+ for (Object obj : objs)
+ {
+ if (needDelim)
+ {
+ ret.append(delim);
+ }
+ if (obj instanceof String)
+ {
+ ret.append('"').append(obj).append('"');
+ }
+ else
+ {
+ ret.append(obj);
+ }
+ needDelim = true;
+ }
+ return ret.toString();
+ }
}
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketServerFactory.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketServerFactory.java
index 56a60ab37e..adc173d696 100644
--- a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketServerFactory.java
+++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketServerFactory.java
@@ -468,7 +468,18 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc
// Initialize / Negotiate Extensions
ExtensionStack extensionStack = new ExtensionStack(getExtensionFactory());
- extensionStack.negotiate(request.getExtensions());
+ // The JSR allows for the extensions to be pre-negotiated, filtered, etc...
+ // Usually from a Configurator.
+ if (response.isExtensionsNegotiated())
+ {
+ // Use pre-negotiated extension list from response
+ extensionStack.negotiate(response.getExtensions());
+ }
+ else
+ {
+ // Use raw extension list from request
+ extensionStack.negotiate(request.getExtensions());
+ }
// Create connection
UpgradeContext context = getActiveUpgradeContext();
@@ -494,6 +505,7 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc
WebSocketSession session = createSession(request.getRequestURI(),driver,connection);
session.setPolicy(driver.getPolicy());
session.setUpgradeRequest(request);
+ // set true negotiated extension list back to response
response.setExtensions(extensionStack.getNegotiatedExtensions());
session.setUpgradeResponse(response);
connection.setSession(session);
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/blockhead/BlockheadClient.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/blockhead/BlockheadClient.java
index 7fe948890c..48eaa58dbb 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/blockhead/BlockheadClient.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/blockhead/BlockheadClient.java
@@ -20,6 +20,7 @@ package org.eclipse.jetty.websocket.server.blockhead;
import static org.hamcrest.Matchers.*;
+import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
@@ -83,7 +84,7 @@ import org.junit.Assert;
* with regards to basic IO behavior, a write should work as expected, a read should work as expected, but <u>what</u> byte it sends or reads is not within its
* scope.
*/
-public class BlockheadClient implements IncomingFrames, OutgoingFrames, ConnectionStateListener
+public class BlockheadClient implements IncomingFrames, OutgoingFrames, ConnectionStateListener, Closeable
{
private static final String REQUEST_HASH_KEY = "dGhlIHNhbXBsZSBub25jZQ==";
private static final int BUFFER_SIZE = 8192;
diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeResponse.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeResponse.java
index c7b50dc8be..eaf4324cd4 100644
--- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeResponse.java
+++ b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeResponse.java
@@ -19,10 +19,12 @@
package org.eclipse.jetty.websocket.servlet;
import java.io.IOException;
+import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.websocket.api.UpgradeResponse;
+import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
/**
* Servlet Specific UpgradeResponse implementation.
@@ -30,6 +32,8 @@ import org.eclipse.jetty.websocket.api.UpgradeResponse;
public class ServletUpgradeResponse extends UpgradeResponse
{
private HttpServletResponse resp;
+ private boolean extensionsNegotiated = false;
+ private boolean subprotocolNegotiated = false;
public ServletUpgradeResponse(HttpServletResponse resp)
{
@@ -60,6 +64,16 @@ public class ServletUpgradeResponse extends UpgradeResponse
return this.resp.isCommitted();
}
+ public boolean isExtensionsNegotiated()
+ {
+ return extensionsNegotiated;
+ }
+
+ public boolean isSubprotocolNegotiated()
+ {
+ return subprotocolNegotiated;
+ }
+
public void sendError(int statusCode, String message) throws IOException
{
setSuccess(false);
@@ -74,6 +88,20 @@ public class ServletUpgradeResponse extends UpgradeResponse
}
@Override
+ public void setAcceptedSubProtocol(String protocol)
+ {
+ super.setAcceptedSubProtocol(protocol);
+ subprotocolNegotiated = true;
+ }
+
+ @Override
+ public void setExtensions(List<ExtensionConfig> extensions)
+ {
+ super.setExtensions(extensions);
+ extensionsNegotiated = true;
+ }
+
+ @Override
public void setHeader(String name, String value)
{
this.resp.setHeader(name,value);

Back to the top