From 6b42a1c45d6c62f9eedd72e547b62f93bac7a253 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Wed, 14 Dec 2011 10:23:40 -0700 Subject: [PATCH] JETTY-1463 - WebSockets with Safari gets messages stuck as if in a buffer that needs to be flushed. + Implementation of Safari WebSocket Draft-0 behavior in a unit test. (Test fails, and is currently set as @Ignore) --- .../websocket/SafariWebsocketDraft0Test.java | 110 +++++++++++++++ .../jetty/websocket/WebSocketCommTest.java | 111 +-------------- .../jetty/websocket/helper/CaptureSocket.java | 42 ++++++ .../jetty/websocket/helper/MessageSender.java | 52 +++++++ .../jetty/websocket/helper/SafariD00.java | 129 ++++++++++++++++++ .../helper/WebSocketCaptureServlet.java | 31 +++++ 6 files changed, 367 insertions(+), 108 deletions(-) create mode 100644 jetty-websocket/src/test/java/org/eclipse/jetty/websocket/SafariWebsocketDraft0Test.java create mode 100644 jetty-websocket/src/test/java/org/eclipse/jetty/websocket/helper/CaptureSocket.java create mode 100644 jetty-websocket/src/test/java/org/eclipse/jetty/websocket/helper/MessageSender.java create mode 100644 jetty-websocket/src/test/java/org/eclipse/jetty/websocket/helper/SafariD00.java create mode 100644 jetty-websocket/src/test/java/org/eclipse/jetty/websocket/helper/WebSocketCaptureServlet.java diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/SafariWebsocketDraft0Test.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/SafariWebsocketDraft0Test.java new file mode 100644 index 00000000000..3d7f2864143 --- /dev/null +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/SafariWebsocketDraft0Test.java @@ -0,0 +1,110 @@ +package org.eclipse.jetty.websocket; + +import static org.hamcrest.Matchers.*; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.log.StdErrLog; +import org.eclipse.jetty.websocket.helper.CaptureSocket; +import org.eclipse.jetty.websocket.helper.SafariD00; +import org.eclipse.jetty.websocket.helper.WebSocketCaptureServlet; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +public class SafariWebsocketDraft0Test +{ + private Server server; + private WebSocketCaptureServlet servlet; + private URI serverUri; + + @BeforeClass + public static void initLogging() + { + // Configure Logging + System.setProperty("org.eclipse.jetty.util.log.class",StdErrLog.class.getName()); + System.setProperty("org.eclipse.jetty.LEVEL","DEBUG"); + } + + @Before + public void startServer() throws Exception + { + // Configure Server + server = new Server(0); + + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/"); + server.setHandler(context); + + // Serve capture servlet + servlet = new WebSocketCaptureServlet(); + context.addServlet(new ServletHolder(servlet),"/"); + + // Start Server + server.start(); + + Connector conn = server.getConnectors()[0]; + String host = conn.getHost(); + if (host == null) + { + host = "localhost"; + } + int port = conn.getLocalPort(); + serverUri = new URI(String.format("ws://%s:%d/",host,port)); + System.out.printf("Server URI: %s%n",serverUri); + } + + @Test + @Ignore + public void testSendTextMessages() throws Exception + { + SafariD00 safari = new SafariD00(serverUri); + + try + { + safari.connect(); + safari.issueHandshake(); + + // Send 5 short messages, using technique seen in Safari. + safari.sendMessage("aa-0"); // single msg + safari.sendMessage("aa-1", "aa-2", "aa-3", "aa-4"); + + // Servlet should show only 1 connection. + Assert.assertThat("Servlet.captureSockets.size",servlet.captures.size(),is(1)); + + CaptureSocket socket = servlet.captures.get(0); + Assert.assertThat("CaptureSocket",socket,notNullValue()); + Assert.assertThat("CaptureSocket.isConnected", socket.isConnected(), is(true)); + + // Give servlet 500 millisecond to process messages + threadSleep(1,TimeUnit.SECONDS); + // Should have captured 5 messages. + Assert.assertThat("CaptureSocket.messages.size",socket.messages.size(),is(5)); + } + finally + { + System.out.println("Closing client socket"); + safari.disconnect(); + } + } + + public static void threadSleep(int dur, TimeUnit unit) throws InterruptedException + { + long ms = TimeUnit.MILLISECONDS.convert(dur,unit); + Thread.sleep(ms); + } + + @After + public void stopServer() throws Exception + { + server.stop(); + } +} diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketCommTest.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketCommTest.java index 88f311d6527..c2017f7c0a9 100644 --- a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketCommTest.java +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketCommTest.java @@ -2,22 +2,17 @@ package org.eclipse.jetty.websocket; import static org.hamcrest.Matchers.*; -import java.io.IOException; import java.net.URI; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.log.StdErrLog; +import org.eclipse.jetty.websocket.helper.CaptureSocket; +import org.eclipse.jetty.websocket.helper.MessageSender; +import org.eclipse.jetty.websocket.helper.WebSocketCaptureServlet; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; @@ -29,106 +24,6 @@ import org.junit.Test; */ public class WebSocketCommTest { - @SuppressWarnings("serial") - private static class WebSocketCaptureServlet extends WebSocketServlet - { - public List captures = new ArrayList();; - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException - { - resp.sendError(404); - } - - public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) - { - CaptureSocket capture = new CaptureSocket(); - captures.add(capture); - return capture; - } - } - - private static class CaptureSocket implements WebSocket, WebSocket.OnTextMessage - { - private Connection conn; - public List messages; - - public CaptureSocket() - { - messages = new ArrayList(); - } - - public boolean isConnected() - { - if (conn == null) - { - return false; - } - return conn.isOpen(); - } - - public void onMessage(String data) - { - System.out.printf("Received Message \"%s\" [size %d]%n", data, data.length()); - messages.add(data); - } - - public void onOpen(Connection connection) - { - this.conn = connection; - } - - public void onClose(int closeCode, String message) - { - this.conn = null; - } - } - - public static class MessageSender implements WebSocket - { - private Connection conn; - private CountDownLatch connectLatch = new CountDownLatch(1); - - public void onOpen(Connection connection) - { - this.conn = connection; - connectLatch.countDown(); - } - - public void onClose(int closeCode, String message) - { - this.conn = null; - } - - public boolean isConnected() - { - if (this.conn == null) - { - return false; - } - return this.conn.isOpen(); - } - - public void sendMessage(String format, Object... args) throws IOException - { - this.conn.sendMessage(String.format(format,args)); - } - - public void awaitConnect() throws InterruptedException - { - connectLatch.await(1,TimeUnit.SECONDS); - } - - public void close() - { - if (this.conn == null) - { - return; - } - this.conn.close(); - } - } - private Server server; private WebSocketCaptureServlet servlet; private URI serverUri; diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/helper/CaptureSocket.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/helper/CaptureSocket.java new file mode 100644 index 00000000000..45842596af3 --- /dev/null +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/helper/CaptureSocket.java @@ -0,0 +1,42 @@ +package org.eclipse.jetty.websocket.helper; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jetty.websocket.WebSocket; + +public class CaptureSocket implements WebSocket, WebSocket.OnTextMessage +{ + private Connection conn; + public List messages; + + public CaptureSocket() + { + messages = new ArrayList(); + } + + public boolean isConnected() + { + if (conn == null) + { + return false; + } + return conn.isOpen(); + } + + public void onMessage(String data) + { + System.out.printf("Received Message \"%s\" [size %d]%n", data, data.length()); + messages.add(data); + } + + public void onOpen(Connection connection) + { + this.conn = connection; + } + + public void onClose(int closeCode, String message) + { + this.conn = null; + } +} \ No newline at end of file diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/helper/MessageSender.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/helper/MessageSender.java new file mode 100644 index 00000000000..1a039ceb192 --- /dev/null +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/helper/MessageSender.java @@ -0,0 +1,52 @@ +package org.eclipse.jetty.websocket.helper; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.websocket.WebSocket; + +public class MessageSender implements WebSocket +{ + private Connection conn; + private CountDownLatch connectLatch = new CountDownLatch(1); + + public void onOpen(Connection connection) + { + this.conn = connection; + connectLatch.countDown(); + } + + public void onClose(int closeCode, String message) + { + this.conn = null; + } + + public boolean isConnected() + { + if (this.conn == null) + { + return false; + } + return this.conn.isOpen(); + } + + public void sendMessage(String format, Object... args) throws IOException + { + this.conn.sendMessage(String.format(format,args)); + } + + public void awaitConnect() throws InterruptedException + { + connectLatch.await(1,TimeUnit.SECONDS); + } + + public void close() + { + if (this.conn == null) + { + return; + } + this.conn.close(); + } +} \ No newline at end of file diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/helper/SafariD00.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/helper/SafariD00.java new file mode 100644 index 00000000000..a1370f092a0 --- /dev/null +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/helper/SafariD00.java @@ -0,0 +1,129 @@ +package org.eclipse.jetty.websocket.helper; + +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.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.URI; + +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.util.TypeUtil; +import org.junit.Assert; + +public class SafariD00 +{ + private URI uri; + private SocketAddress endpoint; + private Socket socket; + private OutputStream out; + private InputStream in; + + public SafariD00(URI uri) + { + this.uri = uri; + this.endpoint = new InetSocketAddress(uri.getHost(),uri.getPort()); + } + + /** + * Open the Socket to the destination endpoint and + * + * @return the open java Socket. + * @throws IOException + */ + public Socket connect() throws IOException + { + socket = new Socket(); + socket.connect(endpoint,1000); + + out = socket.getOutputStream(); + in = socket.getInputStream(); + + return socket; + } + + /** + * Issue an Http websocket (Draft-0) upgrade request using the Safari particulars. + * + * @throws UnsupportedEncodingException + */ + public void issueHandshake() throws IOException + { + StringBuilder req = new StringBuilder(); + req.append("GET ").append(uri.getPath()).append(" HTTP/1.1\r\n"); + req.append("Upgrade: WebSocket\r\n"); + req.append("Connection: Upgrade\r\n"); + req.append("Host: ").append(uri.getHost()).append(":").append(uri.getPort()).append("\r\n"); + req.append("Origin: http://www.google.com/\r\n"); + req.append("Sec-WebSocket-Key1: 15{ft :6@87 0 M 5 c901\r\n"); + req.append("Sec-WebSocket-Key2: 3? C;7~0 8 \" 3 2105 6 `_ {\r\n"); + req.append("\r\n"); + + System.out.printf("--- Request ---%n%s",req); + + byte reqBytes[] = req.toString().getBytes("UTF-8"); + byte hixieBytes[] = TypeUtil.fromHexString("e739617916c9daf3"); + byte buf[] = new byte[reqBytes.length + hixieBytes.length]; + System.arraycopy(reqBytes,0,buf,0,reqBytes.length); + System.arraycopy(hixieBytes,0,buf,reqBytes.length,hixieBytes.length); + + // Send HTTP GET Request (with hixie bytes) + out.write(buf,0,buf.length); + out.flush(); + + // Read HTTP 101 Upgrade / Handshake Response + InputStreamReader reader = new InputStreamReader(in); + BufferedReader br = new BufferedReader(reader); + + boolean foundEnd = false; + String line; + while (!foundEnd) + { + line = br.readLine(); + System.out.printf("RESP: %s%n",line); + if (line.length() == 0) + { + foundEnd = true; + } + } + + // Read expected handshake hixie bytes + byte hixieHandshakeExpected[] = TypeUtil.fromHexString("c7438d956cf611a6af70603e6fa54809"); + byte hixieHandshake[] = new byte[hixieHandshakeExpected.length]; + + int readLen = in.read(hixieHandshake,0,hixieHandshake.length); + Assert.assertThat("Read hixie handshake bytes",readLen,is(hixieHandshake.length)); + } + + public void sendMessage(String... msgs) throws IOException + { + int len = 0; + for (String msg : msgs) + { + len += (msg.length() + 2); + } + + ByteArrayBuffer buf = new ByteArrayBuffer(len); + + for (String msg : msgs) + { + buf.put((byte)0x00); + buf.put(msg.getBytes("UTF-8")); + buf.put((byte)0xFF); + } + + out.write(buf.array()); + out.flush(); + } + + public void disconnect() throws IOException + { + socket.close(); + } +} diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/helper/WebSocketCaptureServlet.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/helper/WebSocketCaptureServlet.java new file mode 100644 index 00000000000..706c4bd7eb4 --- /dev/null +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/helper/WebSocketCaptureServlet.java @@ -0,0 +1,31 @@ +package org.eclipse.jetty.websocket.helper; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.websocket.WebSocket; +import org.eclipse.jetty.websocket.WebSocketServlet; + +@SuppressWarnings("serial") +public class WebSocketCaptureServlet extends WebSocketServlet +{ + public List captures = new ArrayList();; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + resp.sendError(404); + } + + public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) + { + CaptureSocket capture = new CaptureSocket(); + captures.add(capture); + return capture; + } +} \ No newline at end of file