From 6108d0df0c598227c960bc3fffa7efb531eff8d3 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Mon, 29 Oct 2012 11:18:01 -0700 Subject: [PATCH] 393075 Jetty WebSocket client cannot connect to Tomcat WebSocket Server * Adding testcase to repliate behavior as reported by bug. --- .../websocket/WebSocketClientFactory.java | 6 +- .../websocket/TomcatServerQuirksTest.java | 122 +++++++ .../jetty/websocket/WebSocketClientTest.java | 6 +- .../jetty/websocket/dummy/DummyServer.java | 308 ++++++++++++++++++ .../test/resources/jetty-logging.properties | 4 + 5 files changed, 441 insertions(+), 5 deletions(-) create mode 100644 jetty-websocket/src/test/java/org/eclipse/jetty/websocket/TomcatServerQuirksTest.java create mode 100644 jetty-websocket/src/test/java/org/eclipse/jetty/websocket/dummy/DummyServer.java create mode 100644 jetty-websocket/src/test/resources/jetty-logging.properties diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketClientFactory.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketClientFactory.java index d624428b727..b365ee6dfd5 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketClientFactory.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketClientFactory.java @@ -28,6 +28,7 @@ import java.util.Map; import java.util.Queue; import java.util.Random; import java.util.concurrent.ConcurrentLinkedQueue; + import javax.net.ssl.SSLEngine; import org.eclipse.jetty.http.HttpFields; @@ -389,7 +390,7 @@ public class WebSocketClientFactory extends AggregateLifeCycle _accept = value.toString(); } - @Override + @Override // TODO simone says shouldn't be needed public void startRequest(Buffer method, Buffer url, Buffer version) throws IOException { if (_error == null) @@ -397,7 +398,7 @@ public class WebSocketClientFactory extends AggregateLifeCycle _endp.close(); } - @Override + @Override // TODO simone says shouldn't be needed public void content(Buffer ref) throws IOException { if (_error == null) @@ -515,6 +516,7 @@ public class WebSocketClientFactory extends AggregateLifeCycle private WebSocketConnection newWebSocketConnection() throws IOException { + __log.debug("newWebSocketConnection()"); return new WebSocketClientConnection( _future._client.getFactory(), _future.getWebSocket(), diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/TomcatServerQuirksTest.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/TomcatServerQuirksTest.java new file mode 100644 index 00000000000..754a4199db4 --- /dev/null +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/TomcatServerQuirksTest.java @@ -0,0 +1,122 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 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; + +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.websocket.dummy.DummyServer; +import org.eclipse.jetty.websocket.dummy.DummyServer.ServerConnection; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +public class TomcatServerQuirksTest +{ + /** + * Test for when encountering a "Transfer-Encoding: chunked" on a Upgrade Response header. + * + * @throws IOException + */ + @Test + @Ignore("Bug with Transfer-Encoding") + public void testTomcat7_0_32_WithTransferEncoding() throws Exception { + DummyServer server = new DummyServer(); + int bufferSize = 512; + QueuedThreadPool threadPool = new QueuedThreadPool(); + WebSocketClientFactory factory = new WebSocketClientFactory(threadPool, new ZeroMaskGen(), bufferSize); + + try { + server.start(); + + // Setup Client Factory + threadPool.start(); + factory.start(); + + // Create Client + WebSocketClient client = new WebSocketClient(factory); + + // Create End User WebSocket Class + final CountDownLatch openLatch = new CountDownLatch(1); + final CountDownLatch dataLatch = new CountDownLatch(1); + WebSocket.OnTextMessage websocket = new WebSocket.OnTextMessage() + { + public void onOpen(Connection connection) + { + openLatch.countDown(); + } + + public void onMessage(String data) + { + // System.out.println("data = " + data); + dataLatch.countDown(); + } + + public void onClose(int closeCode, String message) + { + } + }; + + // Open connection + URI wsURI = server.getWsUri(); + client.open(wsURI, websocket); + + // Accept incoming connection + ServerConnection socket = server.accept(); + socket.setSoTimeout(2000); // timeout + + // Issue upgrade + Map extraResponseHeaders = new HashMap(); + extraResponseHeaders.put("Transfer-Encoding", "chunked"); // !! The problem !! + socket.upgrade(extraResponseHeaders); + + // Wait for proper upgrade + Assert.assertTrue("Timed out waiting for Client side WebSocket open event", openLatch.await(1, TimeUnit.SECONDS)); + + // Have server write frame. + int length = bufferSize / 2; + ByteBuffer serverFrame = ByteBuffer.allocate(bufferSize); + serverFrame.put((byte)(0x80 | 0x01)); // FIN + TEXT + serverFrame.put((byte)0x7E); // No MASK and 2 bytes length + serverFrame.put((byte)(length >> 8)); // first length byte + serverFrame.put((byte)(length & 0xFF)); // second length byte + for (int i = 0; i < length; ++i) + serverFrame.put((byte)'x'); + serverFrame.flip(); + byte buf[] = serverFrame.array(); + socket.write(buf,0,buf.length); + socket.flush(); + + Assert.assertTrue(dataLatch.await(1000, TimeUnit.SECONDS)); + } finally { + factory.stop(); + threadPool.stop(); + server.stop(); + } + } +} diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketClientTest.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketClientTest.java index f70c43ca36a..8cd1b467c11 100644 --- a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketClientTest.java +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketClientTest.java @@ -18,6 +18,8 @@ package org.eclipse.jetty.websocket; +import static org.hamcrest.Matchers.*; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -47,9 +49,6 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.is; - public class WebSocketClientTest { private WebSocketClientFactory _factory = new WebSocketClientFactory(); @@ -103,6 +102,7 @@ public class WebSocketClientTest { } }; + client.open(new URI("ws://127.0.0.1:" + _serverPort + "/"), websocket); Socket socket = _server.accept(); diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/dummy/DummyServer.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/dummy/DummyServer.java new file mode 100644 index 00000000000..9b41e51e2fa --- /dev/null +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/dummy/DummyServer.java @@ -0,0 +1,308 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 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.dummy; + +import static org.hamcrest.Matchers.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.WebSocketConnectionRFC6455; +import org.junit.Assert; + +/** + * Simple ServerSocket server used to test oddball server scenarios encountered in the real world. + */ +public class DummyServer +{ + public static class ServerConnection + { + private static final Logger LOG = Log.getLogger(ServerConnection.class); + private final Socket socket; + private InputStream in; + private OutputStream out; + + public ServerConnection(Socket socket) + { + this.socket = socket; + } + + public int read(ByteBuffer buf) throws IOException + { + int len = 0; + while ((in.available() > 0) && (buf.remaining() > 0)) + { + buf.put((byte)in.read()); + len++; + } + return len; + } + + public void disconnect() + { + LOG.debug("disconnect"); + IO.close(in); + IO.close(out); + if (socket != null) + { + try + { + socket.close(); + } + catch (IOException ignore) + { + /* ignore */ + } + } + } + + public InputStream getInputStream() throws IOException + { + if (in == null) + { + in = socket.getInputStream(); + } + return in; + } + + public OutputStream getOutputStream() throws IOException + { + if (out == null) + { + out = socket.getOutputStream(); + } + return out; + } + + public void flush() throws IOException + { + LOG.debug("flush()"); + getOutputStream().flush(); + } + + public String readRequest() throws IOException + { + LOG.debug("Reading client request"); + StringBuilder request = new StringBuilder(); + BufferedReader in = new BufferedReader(new InputStreamReader(getInputStream())); + for (String line = in.readLine(); line != null; line = in.readLine()) + { + if (line.length() == 0) + { + break; + } + request.append(line).append("\r\n"); + LOG.debug("read line: {}",line); + } + + LOG.debug("Client Request:{}{}","\n",request); + return request.toString(); + } + + public void respond(String rawstr) throws IOException + { + LOG.debug("respond(){}{}","\n",rawstr); + getOutputStream().write(rawstr.getBytes()); + flush(); + } + + public void setSoTimeout(int ms) throws SocketException + { + socket.setSoTimeout(ms); + } + + public void upgrade(Map extraResponseHeaders) throws IOException + { + @SuppressWarnings("unused") + Pattern patExts = Pattern.compile("^Sec-WebSocket-Extensions: (.*)$",Pattern.CASE_INSENSITIVE); + Pattern patKey = Pattern.compile("^Sec-WebSocket-Key: (.*)$",Pattern.CASE_INSENSITIVE); + + LOG.debug("(Upgrade) Reading HTTP Request"); + Matcher mat; + String key = "not sent"; + BufferedReader in = new BufferedReader(new InputStreamReader(getInputStream())); + for (String line = in.readLine(); line != null; line = in.readLine()) + { + if (line.length() == 0) + { + break; + } + + // TODO: Check for extensions + // mat = patExts.matcher(line); + // if (mat.matches()) + + // Check for Key + mat = patKey.matcher(line); + if (mat.matches()) + { + key = mat.group(1); + } + } + + LOG.debug("(Upgrade) Writing HTTP Response"); + // TODO: handle extensions? + + // Setup Response + StringBuilder resp = new StringBuilder(); + resp.append("HTTP/1.1 101 Upgrade\r\n"); + resp.append("Upgrade: websocket\r\n"); + resp.append("Connection: upgrade\r\n"); + resp.append("Sec-WebSocket-Accept: "); + resp.append(WebSocketConnectionRFC6455.hashKey(key)).append("\r\n"); + // extra response headers. + if (extraResponseHeaders != null) + { + for (Map.Entry header : extraResponseHeaders.entrySet()) + { + resp.append(header.getKey()); + resp.append(": "); + resp.append(header.getValue()); + resp.append("\r\n"); + } + } + resp.append("\r\n"); + + // Write Response + getOutputStream().write(resp.toString().getBytes()); + flush(); + } + + public void write(byte[] bytes) throws IOException + { + LOG.debug("Writing {} bytes", bytes.length); + getOutputStream().write(bytes); + } + + public void write(byte[] buf, int offset, int length) throws IOException + { + LOG.debug("Writing bytes[{}], offset={}, length={}", buf.length, offset, length); + getOutputStream().write(buf,offset,length); + } + + public void write(int b) throws IOException + { + LOG.debug("Writing int={}", b); + getOutputStream().write(b); + } + } + + private static final Logger LOG = Log.getLogger(DummyServer.class); + private ServerSocket serverSocket; + private URI wsUri; + + public ServerConnection accept() throws IOException + { + LOG.debug(".accept()"); + assertIsStarted(); + Socket socket = serverSocket.accept(); + return new ServerConnection(socket); + } + + private void assertIsStarted() + { + Assert.assertThat("ServerSocket",serverSocket,notNullValue()); + Assert.assertThat("ServerSocket.isBound",serverSocket.isBound(),is(true)); + Assert.assertThat("ServerSocket.isClosed",serverSocket.isClosed(),is(false)); + + Assert.assertThat("WsUri",wsUri,notNullValue()); + } + + public URI getWsUri() + { + return wsUri; + } + + public void respondToClient(Socket connection, String serverResponse) throws IOException + { + InputStream in = null; + InputStreamReader isr = null; + BufferedReader buf = null; + OutputStream out = null; + try + { + in = connection.getInputStream(); + isr = new InputStreamReader(in); + buf = new BufferedReader(isr); + String line; + while ((line = buf.readLine()) != null) + { + // System.err.println(line); + if (line.length() == 0) + { + // Got the "\r\n" line. + break; + } + } + + // System.out.println("[Server-Out] " + serverResponse); + out = connection.getOutputStream(); + out.write(serverResponse.getBytes()); + out.flush(); + } + finally + { + IO.close(buf); + IO.close(isr); + IO.close(in); + IO.close(out); + } + } + + public void start() throws IOException + { + serverSocket = new ServerSocket(); + InetAddress addr = InetAddress.getByName("localhost"); + InetSocketAddress endpoint = new InetSocketAddress(addr,0); + serverSocket.bind(endpoint); + int port = serverSocket.getLocalPort(); + String uri = String.format("ws://%s:%d/",addr.getHostAddress(),port); + wsUri = URI.create(uri); + LOG.debug("Server Started on {} -> {}",endpoint,wsUri); + } + + public void stop() + { + LOG.debug("Stopping Server"); + try + { + serverSocket.close(); + } + catch (IOException ignore) + { + /* ignore */ + } + } + +} diff --git a/jetty-websocket/src/test/resources/jetty-logging.properties b/jetty-websocket/src/test/resources/jetty-logging.properties new file mode 100644 index 00000000000..78d8a9d8123 --- /dev/null +++ b/jetty-websocket/src/test/resources/jetty-logging.properties @@ -0,0 +1,4 @@ +# Setup default logging implementation for during testing +org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog +org.eclipse.jetty.LEVEL=INFO +org.eclipse.jetty.websocket.LEVEL=DEBUG \ No newline at end of file