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 f8fa85a73c9..0cc7ad4674b 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 1c050f6628b..633418c3148 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 d95c46977bd..5137303f745 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 getNegotiatedExtensions(List installed, List 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 0bde7dab945..c4b652adda8 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 installedExts = new ArrayList<>(); + for (String extName : extensionFactory.getAvailableExtensions().keySet()) + { + installedExts.add(new JsrExtension(extName)); + } + List requestedExts = new ArrayList<>(); + for (ExtensionConfig reqCfg : req.getExtensions()) + { + requestedExts.add(new JsrExtension(reqCfg)); + } + List usedExts = configurator.getNegotiatedExtensions(installedExts,requestedExts); + List 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 2bd5c36398e..9a18da3dbc0 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 00000000000..dae78b6cde6 --- /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 getNegotiatedExtensions(List installed, List 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> headers = (Map>)session.getUserProperties().get("request-headers"); + if (headers == null) + { + response.append(""); + } + else + { + List values = headers.get(headerKey); + if (values == null) + { + response.append("
"); + } + 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 d35fe99cab5..108099af205 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 00000000000..13739fb4776 --- /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. + *

+ * This client will use {@link HttpURLConnection} and {@link HttpsURLConnection} with standard blocking calls to perform websocket requests. + *

+ * This client is NOT 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. + *

+ * 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 what 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 extensions = new ArrayList<>(); + private List 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 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 getExtensionConfigs(HttpResponse response) + { + List 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 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 00000000000..a88ed7ca680 --- /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 data() + { + List 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 00000000000..88f52ebe249 --- /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 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 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 00000000000..cc7f574b8c1 --- /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 frames = new EventQueue<>(); + private EventQueue 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 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 errorType) + { + int count = 0; + for (Throwable error : errors) + { + if (errorType.isInstance(error)) + { + count++; + } + } + return count; + } + + public Queue getErrors() + { + return errors; + } + + public int getFrameCount(byte op) + { + int count = 0; + for (WebSocketFrame frame : frames) + { + if (frame.getOpCode() == op) + { + count++; + } + } + return count; + } + + public Queue 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 00000000000..0c5be547dca --- /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 value = request.getHeaders().get("User-Agent"); + return QuoteUtil.join(value,","); + } + + @Override + public List getNegotiatedExtensions(List installed, List 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 00000000000..ae91e49b770 --- /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. + *

+ * 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 00000000000..0ea19c99fb1 --- /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 88b96eead11..c5a50f62391 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 00000000000..ee9ef00de39 --- /dev/null +++ b/jetty-websocket/javax-websocket-server-impl/src/test/resources/jsr-browser-debug-tool/index.html @@ -0,0 +1,37 @@ + + + Jetty WebSocket Browser -> Server Debug Tool + + + + + jetty websocket/browser/javascript -> server debug tool #console +

+
+ + + + + + + + + +
+ + + \ 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 00000000000..9eebead468d --- /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 00000000000..1bc5fe78947 --- /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 2e17cef9b3d..eed6f20ed1d 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 56a60ab37eb..adc173d6966 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 7fe948890c1..48eaa58dbb4 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 what 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 c7b50dc8beb..eaf4324cd40 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); @@ -73,6 +87,20 @@ public class ServletUpgradeResponse extends UpgradeResponse resp.sendError(HttpServletResponse.SC_FORBIDDEN,message); } + @Override + public void setAcceptedSubProtocol(String protocol) + { + super.setAcceptedSubProtocol(protocol); + subprotocolNegotiated = true; + } + + @Override + public void setExtensions(List extensions) + { + super.setExtensions(extensions); + extensionsNegotiated = true; + } + @Override public void setHeader(String name, String value) {