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