You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2015/12/04 15:05:49 UTC

svn commit: r1717965 - in /tomcat/trunk/test/org/apache/tomcat/websocket/server: TestClose.java TesterWsCloseClient.java

Author: markt
Date: Fri Dec  4 14:05:49 2015
New Revision: 1717965

URL: http://svn.apache.org/viewvc?rev=1717965&view=rev
Log:
Add test cases, currently disabled because they don't all pass, for various issues around WebSocket closing.
Patch by Barry Coughlan 

Added:
    tomcat/trunk/test/org/apache/tomcat/websocket/server/TestClose.java   (with props)
    tomcat/trunk/test/org/apache/tomcat/websocket/server/TesterWsCloseClient.java   (with props)

Added: tomcat/trunk/test/org/apache/tomcat/websocket/server/TestClose.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/tomcat/websocket/server/TestClose.java?rev=1717965&view=auto
==============================================================================
--- tomcat/trunk/test/org/apache/tomcat/websocket/server/TestClose.java (added)
+++ tomcat/trunk/test/org/apache/tomcat/websocket/server/TestClose.java Fri Dec  4 14:05:49 2015
@@ -0,0 +1,347 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.ServletContextEvent;
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCode;
+import javax.websocket.CloseReason.CloseCodes;
+import javax.websocket.DeploymentException;
+import javax.websocket.OnClose;
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerContainer;
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.servlets.DefaultServlet;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
+
+/**
+ * Test the behavior of closing websockets under various conditions.
+ */
+@Ignore // Only because they don't pass at the moment.
+public class TestClose extends TomcatBaseTest {
+
+    // TODO: These are static because I'm not sure how to inject them to the
+    // endpoint
+    private static volatile Events events;
+
+
+    public static class Events {
+        // Used to block in the @OnMessage
+        public final CountDownLatch onMessageWait = new CountDownLatch(1);
+
+        // Used to check which methods of a server endpoint were called
+        public final CountDownLatch onErrorCalled = new CountDownLatch(1);
+        public final CountDownLatch onMessageCalled = new CountDownLatch(1);
+        public final CountDownLatch onCloseCalled = new CountDownLatch(1);
+
+        // Parameter of an @OnClose call
+        public volatile CloseReason closeReason = null;
+        // Parameter of an @OnError call
+        public volatile Throwable onErrorThrowable = null;
+
+        //This is set to true for tests where the @OnMessage should send a message
+        public volatile boolean onMessageSends = false;
+    }
+
+
+    private static void awaitLatch(CountDownLatch latch, String failMessage) {
+        try {
+            if (!latch.await(3000, TimeUnit.MILLISECONDS)) {
+                Assert.fail(failMessage);
+            }
+        } catch (InterruptedException e) {
+            // Won't happen
+            throw new RuntimeException(e);
+        }
+    }
+
+
+    public static void awaitOnClose(CloseCode code) {
+        awaitLatch(events.onCloseCalled, "onClose not called");
+        Assert.assertEquals(code.getCode(), events.closeReason.getCloseCode()
+                .getCode());
+    }
+
+
+    public static void awaitOnError(Class<? extends Throwable> exceptionClazz) {
+        awaitLatch(events.onErrorCalled, "onError not called");
+        Assert.assertEquals(exceptionClazz, events.onErrorThrowable.getClass());
+    }
+
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        events = new Events();
+    }
+
+
+    @Test
+    public void testTcpClose() throws Exception {
+        startServer(TestEndpointConfig.class);
+
+        TesterWsCloseClient client = new TesterWsCloseClient("localhost", getPort());
+        client.httpUpgrade(BaseEndpointConfig.PATH);
+        client.closeSocket();
+
+        awaitOnClose(CloseCodes.CLOSED_ABNORMALLY);
+    }
+
+
+    @Test
+    public void testTcpReset() throws Exception {
+        startServer(TestEndpointConfig.class);
+
+        TesterWsCloseClient client = new TesterWsCloseClient("localhost", getPort());
+        client.httpUpgrade(BaseEndpointConfig.PATH);
+        client.forceCloseSocket();
+
+        // TODO: I'm not entirely sure when onError should be called
+        awaitOnError(IOException.class);
+        awaitOnClose(CloseCodes.CLOSED_ABNORMALLY);
+    }
+
+
+    @Test
+    public void testWsCloseThenTcpClose() throws Exception {
+        startServer(TestEndpointConfig.class);
+
+        TesterWsCloseClient client = new TesterWsCloseClient("localhost", getPort());
+        client.httpUpgrade(BaseEndpointConfig.PATH);
+        client.sendCloseFrame(CloseCodes.GOING_AWAY);
+        client.closeSocket();
+
+        awaitOnClose(CloseCodes.GOING_AWAY);
+    }
+
+
+    @Test
+    public void testWsCloseThenTcpReset() throws Exception {
+        startServer(TestEndpointConfig.class);
+
+        TesterWsCloseClient client = new TesterWsCloseClient("localhost", getPort());
+        client.httpUpgrade(BaseEndpointConfig.PATH);
+        client.sendCloseFrame(CloseCodes.GOING_AWAY);
+        client.forceCloseSocket();
+
+        //TODO: Why CLOSED_ABNORMALLY when above is GOING_AWAY?
+        awaitOnClose(CloseCodes.CLOSED_ABNORMALLY);
+    }
+
+
+    @Test
+    public void testTcpCloseInOnMessage() throws Exception {
+        startServer(TestEndpointConfig.class);
+
+        TesterWsCloseClient client = new TesterWsCloseClient("localhost", getPort());
+        client.httpUpgrade(BaseEndpointConfig.PATH);
+        client.sendMessage("Test");
+        awaitLatch(events.onMessageCalled, "onMessage not called");
+
+        client.closeSocket();
+        events.onMessageWait.countDown();
+
+        awaitOnClose(CloseCodes.CLOSED_ABNORMALLY);
+    }
+
+
+    @Test
+    public void testTcpResetInOnMessage() throws Exception {
+        startServer(TestEndpointConfig.class);
+
+        TesterWsCloseClient client = new TesterWsCloseClient("localhost", getPort());
+        client.httpUpgrade(BaseEndpointConfig.PATH);
+        client.sendMessage("Test");
+        awaitLatch(events.onMessageCalled, "onMessage not called");
+
+        client.forceCloseSocket();
+        events.onMessageWait.countDown();
+
+        awaitOnError(IOException.class);
+        awaitOnClose(CloseCodes.CLOSED_ABNORMALLY);
+    }
+
+
+    @Test
+    public void testWsCloseThenTcpCloseInOnMessage() throws Exception {
+        startServer(TestEndpointConfig.class);
+
+        TesterWsCloseClient client = new TesterWsCloseClient("localhost", getPort());
+        client.httpUpgrade(BaseEndpointConfig.PATH);
+        client.sendMessage("Test");
+        awaitLatch(events.onMessageCalled, "onMessage not called");
+
+        client.sendCloseFrame(CloseCodes.NORMAL_CLOSURE);
+        client.closeSocket();
+        events.onMessageWait.countDown();
+
+        awaitOnClose(CloseCodes.CLOSED_ABNORMALLY);
+    }
+
+
+    @Test
+    public void testWsCloseThenTcpResetInOnMessage() throws Exception {
+        startServer(TestEndpointConfig.class);
+
+        TesterWsCloseClient client = new TesterWsCloseClient("localhost", getPort());
+        client.httpUpgrade(BaseEndpointConfig.PATH);
+        client.sendMessage("Test");
+        awaitLatch(events.onMessageCalled, "onMessage not called");
+
+        client.sendCloseFrame(CloseCodes.NORMAL_CLOSURE);
+        client.closeSocket();
+        events.onMessageWait.countDown();
+
+        awaitOnClose(CloseCodes.CLOSED_ABNORMALLY);
+    }
+
+
+    @Test
+    public void testTcpCloseWhenOnMessageSends() throws Exception {
+        events.onMessageSends = true;
+        testTcpCloseInOnMessage();
+    }
+
+
+    @Test
+    public void testTcpResetWhenOnMessageSends() throws Exception {
+        events.onMessageSends = true;
+        testTcpResetInOnMessage();
+    }
+
+
+    @Test
+    public void testWsCloseThenTcpCloseWhenOnMessageSends() throws Exception {
+        events.onMessageSends = true;
+        testWsCloseThenTcpCloseInOnMessage();
+    }
+
+
+    @Test
+    public void testWsCloseThenTcpResetWhenOnMessageSends() throws Exception {
+        events.onMessageSends = true;
+        testWsCloseThenTcpResetInOnMessage();
+    }
+
+
+    public static class TestEndpoint {
+
+        @OnOpen
+        public void onOpen() {
+            System.out.println("Session opened");
+        }
+
+        @OnMessage
+        public void onMessage(Session session, String message) {
+            System.out.println("Message received: " + message);
+            events.onMessageCalled.countDown();
+            awaitLatch(events.onMessageWait, "onMessageWait not triggered");
+
+            if (events.onMessageSends) {
+                try {
+                    session.getBasicRemote().sendText("Test reply");
+                } catch (IOException e) {
+                    // Expected to fail
+                }
+            }
+        }
+
+        @OnError
+        public void onError(Throwable t) {
+            System.out.println("onError: " + t.getMessage());
+            events.onErrorThrowable = t;
+            events.onErrorCalled.countDown();
+        }
+
+        @OnClose
+        public void onClose(CloseReason cr) {
+            System.out.println("onClose: " + cr);
+            events.closeReason = cr;
+            events.onCloseCalled.countDown();
+        }
+    }
+
+
+    public static class TestEndpointConfig extends BaseEndpointConfig {
+
+        @Override
+        protected Class<?> getEndpointClass() {
+            return TestEndpoint.class;
+        }
+
+    }
+
+
+    private Tomcat startServer(
+            final Class<? extends WsContextListener> configClass)
+            throws LifecycleException {
+
+        Tomcat tomcat = getTomcatInstance();
+        // No file system docBase required
+        Context ctx = tomcat.addContext("", null);
+        ctx.addApplicationListener(configClass.getName());
+        Tomcat.addServlet(ctx, "default", new DefaultServlet());
+        ctx.addServletMapping("/", "default");
+
+        tomcat.start();
+        return tomcat;
+    }
+
+
+    public abstract static class BaseEndpointConfig extends WsContextListener {
+
+        public static final String PATH = "/test";
+
+        protected abstract Class<?> getEndpointClass();
+
+        @Override
+        public void contextInitialized(ServletContextEvent sce) {
+            super.contextInitialized(sce);
+
+            ServerContainer sc = (ServerContainer) sce
+                    .getServletContext()
+                    .getAttribute(
+                            Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE);
+
+            ServerEndpointConfig sec = ServerEndpointConfig.Builder.create(
+                    getEndpointClass(), PATH).build();
+
+            try {
+                sc.addEndpoint(sec);
+            } catch (DeploymentException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+}

Propchange: tomcat/trunk/test/org/apache/tomcat/websocket/server/TestClose.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: tomcat/trunk/test/org/apache/tomcat/websocket/server/TesterWsCloseClient.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/tomcat/websocket/server/TesterWsCloseClient.java?rev=1717965&view=auto
==============================================================================
--- tomcat/trunk/test/org/apache/tomcat/websocket/server/TesterWsCloseClient.java (added)
+++ tomcat/trunk/test/org/apache/tomcat/websocket/server/TesterWsCloseClient.java Fri Dec  4 14:05:49 2015
@@ -0,0 +1,126 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+
+import javax.websocket.CloseReason.CloseCode;
+
+/**
+ * A client for testing Websocket behavior that differs from standard client
+ * behavior.
+ */
+public class TesterWsCloseClient {
+
+    private static final byte[] maskingKey = new byte[] { 0x12, 0x34, 0x56,
+            0x78 };
+
+    private final Socket socket;
+
+    public TesterWsCloseClient(String host, int port) throws Exception {
+        this.socket = new Socket(host, port);
+        // Set read timeout in case of failure so test doesn't hang
+        socket.setSoTimeout(2000);
+        // Disable Nagle's algorithm to ensure packets sent immediately
+        // TODO: Hoping this causes writes to wait for a TCP ACK for TCP RST
+        // test cases but I'm not sure?
+        socket.setTcpNoDelay(true);
+    }
+
+    public void httpUpgrade(String path) throws IOException {
+        String req = createUpgradeRequest(path);
+        write(req.getBytes(StandardCharsets.UTF_8));
+        readUpgradeResponse();
+    }
+
+    public void sendMessage(String text) throws IOException {
+        write(createFrame(true, 1, text.getBytes(StandardCharsets.UTF_8)));
+    }
+
+    public void sendCloseFrame(CloseCode closeCode) throws IOException {
+        int code = closeCode.getCode();
+        byte[] codeBytes = new byte[2];
+        codeBytes[0] = (byte) (code >> 8);
+        codeBytes[1] = (byte) code;
+        write(createFrame(true, 8, codeBytes));
+    }
+
+    private void readUpgradeResponse() throws IOException {
+        BufferedReader in = new BufferedReader(new InputStreamReader(
+                socket.getInputStream()));
+        while (!in.readLine().isEmpty()) {
+
+        }
+    }
+
+    public void closeSocket() throws IOException {
+        // Enable SO_LINGER to ensure close() only returns when TCP closing
+        // handshake completes
+        socket.setSoLinger(true, 65535);
+        socket.close();
+    }
+
+    /**
+     * Send a TCP RST instead of a TCP closing handshake
+     */
+    public void forceCloseSocket() throws IOException {
+        // SO_LINGER sends a TCP RST when timeout expires
+        socket.setSoLinger(true, 0);
+        socket.close();
+    }
+
+    private void write(byte[] bytes) throws IOException {
+        socket.getOutputStream().write(bytes);
+        socket.getOutputStream().flush();
+    }
+
+    private static String createUpgradeRequest(String path) {
+        String[] upgradeRequestLines = { "GET " + path + " HTTP/1.1",
+                "Connection: Upgrade", "Host: localhost:8080",
+                "Origin: localhost:8080",
+                "Sec-WebSocket-Key: OEvAoAKn5jsuqv2/YJ1Wfg==",
+                "Sec-WebSocket-Version: 13", "Upgrade: websocket" };
+        StringBuffer sb = new StringBuffer();
+        for (String line : upgradeRequestLines) {
+            sb.append(line);
+            sb.append("\r\n");
+        }
+        sb.append("\r\n");
+        return sb.toString();
+    }
+
+    private static byte[] createFrame(boolean fin, int opCode, byte[] payload) {
+        byte[] frame = new byte[6 + payload.length];
+        frame[0] = (byte) (opCode + (fin ? 1 << 7 : 0));
+        frame[1] += 0b10000000 + payload.length;
+
+        frame[2] = maskingKey[0];
+        frame[3] = maskingKey[1];
+        frame[4] = maskingKey[2];
+        frame[5] = maskingKey[3];
+
+        for (int i = 0; i < payload.length; i++) {
+            frame[i + 6] = (byte) (payload[i] ^ maskingKey[i % 4]);
+        }
+
+        return frame;
+    }
+}

Propchange: tomcat/trunk/test/org/apache/tomcat/websocket/server/TesterWsCloseClient.java
------------------------------------------------------------------------------
    svn:eol-style = native



---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org