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-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org