diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/IPAccessHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/IPAccessHandlerTest.java new file mode 100644 index 00000000000..be13ae5bfb9 --- /dev/null +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/IPAccessHandlerTest.java @@ -0,0 +1,545 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 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.server.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.BufferedReader; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.NetworkConnector; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class IPAccessHandlerTest +{ + private static Server _server; + private static NetworkConnector _connector; + private static IPAccessHandler _handler; + + @BeforeAll + public static void setUp() + throws Exception + { + _server = new Server(); + _connector = new ServerConnector(_server); + _server.setConnectors(new Connector[] { _connector }); + + _handler = new IPAccessHandler(); + _handler.setHandler(new AbstractHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + response.setStatus(HttpStatus.OK_200); + } + }); + _server.setHandler(_handler); + _server.start(); + } + + /* ------------------------------------------------------------ */ + @AfterAll + public static void tearDown() + throws Exception + { + _server.stop(); + } + + /* ------------------------------------------------------------ */ + @ParameterizedTest + @MethodSource("data") + public void testHandler(String white, String black, String host, String uri, String code, boolean byPath) + throws Exception + { + _handler.setWhite(white.split(";",-1)); + _handler.setBlack(black.split(";",-1)); + _handler.setWhiteListByPath(byPath); + + String request = "GET " + uri + " HTTP/1.1\n" + "Host: "+ host + "\n\n"; + Socket socket = new Socket("127.0.0.1", _connector.getLocalPort()); + socket.setSoTimeout(5000); + try + { + OutputStream output = socket.getOutputStream(); + BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + Response response = readResponse(input); + Object[] params = new Object[]{ + "Request WBHUC", white, black, host, uri, code, + "Response", response.getCode()}; + assertEquals(code, response.getCode(), Arrays.deepToString(params)); + } + finally + { + socket.close(); + } + } + + /* ------------------------------------------------------------ */ + protected Response readResponse(BufferedReader reader) + throws IOException + { + // Simplified parser for HTTP responses + String line = reader.readLine(); + if (line == null) + throw new EOFException(); + Matcher responseLine = Pattern.compile("HTTP/1\\.1\\s+(\\d+)").matcher(line); + assertTrue(responseLine.lookingAt()); + String code = responseLine.group(1); + + Map headers = new LinkedHashMap<>(); + while ((line = reader.readLine()) != null) + { + if (line.trim().length() == 0) + break; + + Matcher header = Pattern.compile("([^:]+):\\s*(.*)").matcher(line); + assertTrue(header.lookingAt()); + String headerName = header.group(1); + String headerValue = header.group(2); + headers.put(headerName.toLowerCase(Locale.ENGLISH), headerValue.toLowerCase(Locale.ENGLISH)); + } + + StringBuilder body = new StringBuilder(); + if (headers.containsKey("content-length")) + { + int length = Integer.parseInt(headers.get("content-length")); + for (int i = 0; i < length; ++i) + { + char c = (char)reader.read(); + body.append(c); + } + } + else if ("chunked".equals(headers.get("transfer-encoding"))) + { + while ((line = reader.readLine()) != null) + { + if ("0".equals(line)) + { + line = reader.readLine(); + assertEquals("", line); + break; + } + + int length = Integer.parseInt(line, 16); + for (int i = 0; i < length; ++i) + { + char c = (char)reader.read(); + body.append(c); + } + line = reader.readLine(); + assertEquals("", line); + } + } + + return new Response(code, headers, body.toString().trim()); + } + + /* ------------------------------------------------------------ */ + protected class Response + { + private final String code; + private final Map headers; + private final String body; + + /* ------------------------------------------------------------ */ + private Response(String code, Map headers, String body) + { + this.code = code; + this.headers = headers; + this.body = body; + } + + /* ------------------------------------------------------------ */ + public String getCode() + { + return code; + } + + /* ------------------------------------------------------------ */ + public Map getHeaders() + { + return headers; + } + + /* ------------------------------------------------------------ */ + public String getBody() + { + return body; + } + + /* ------------------------------------------------------------ */ + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(); + builder.append(code).append("\r\n"); + for (Map.Entry entry : headers.entrySet()) + builder.append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n"); + builder.append("\r\n"); + builder.append(body); + return builder.toString(); + } + } + + /* ------------------------------------------------------------ */ + public static Stream data() { + Object[][] data = new Object[][] { + // Empty lists + {"", "", "127.0.0.1", "/", "200", false}, + {"", "", "127.0.0.1", "/dump/info", "200", false}, + + // White list + {"127.0.0.1", "", "127.0.0.1", "/", "200", false}, + {"127.0.0.1", "", "127.0.0.1", "/dispatch", "200", false}, + {"127.0.0.1", "", "127.0.0.1", "/dump/info", "200", false}, + + {"127.0.0.1|/", "", "127.0.0.1", "/", "200", false}, + {"127.0.0.1|/", "", "127.0.0.1", "/dispatch", "403", false}, + {"127.0.0.1|/", "", "127.0.0.1", "/dump/info", "403", false}, + + {"127.0.0.1|/*", "", "127.0.0.1", "/", "200", false}, + {"127.0.0.1|/*", "", "127.0.0.1", "/dispatch", "200", false}, + {"127.0.0.1|/*", "", "127.0.0.1", "/dump/info", "200", false}, + + {"127.0.0.1|/dump/*", "", "127.0.0.1", "/", "403", false}, + {"127.0.0.1|/dump/*", "", "127.0.0.1", "/dispatch", "403", false}, + {"127.0.0.1|/dump/*", "", "127.0.0.1", "/dump/info", "200", false}, + {"127.0.0.1|/dump/*", "", "127.0.0.1", "/dump/test", "200", false}, + + {"127.0.0.1|/dump/info", "", "127.0.0.1", "/", "403", false}, + {"127.0.0.1|/dump/info", "", "127.0.0.1", "/dispatch", "403", false}, + {"127.0.0.1|/dump/info", "", "127.0.0.1", "/dump/info", "200", false}, + {"127.0.0.1|/dump/info", "", "127.0.0.1", "/dump/test", "403", false}, + + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/", "403", false}, + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/dispatch", "403", false}, + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/dump/info", "200", false}, + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/dump/test", "200", false}, + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/dump/fail", "403", false}, + + {"127.0.0.0-2|", "", "127.0.0.1", "/", "200", false}, + {"127.0.0.0-2|", "", "127.0.0.1", "/dump/info", "403", false}, + + {"127.0.0.0-2|/", "", "127.0.0.1", "/", "200", false}, + {"127.0.0.0-2|/", "", "127.0.0.1", "/dispatch", "403", false}, + {"127.0.0.0-2|/", "", "127.0.0.1", "/dump/info", "403", false}, + + {"127.0.0.0-2|/dump/*", "", "127.0.0.1", "/", "403", false}, + {"127.0.0.0-2|/dump/*", "", "127.0.0.1", "/dispatch", "403", false}, + {"127.0.0.0-2|/dump/*", "", "127.0.0.1", "/dump/info", "200", false}, + + {"127.0.0.0-2|/dump/info", "", "127.0.0.1", "/", "403", false}, + {"127.0.0.0-2|/dump/info", "", "127.0.0.1", "/dispatch", "403", false}, + {"127.0.0.0-2|/dump/info", "", "127.0.0.1", "/dump/info", "200", false}, + {"127.0.0.0-2|/dump/info", "", "127.0.0.1", "/dump/test", "403", false}, + + {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/", "403", false}, + {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/dispatch", "403", false}, + {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/dump/info", "200", false}, + {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/dump/test", "200", false}, + {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/dump/fail", "403", false}, + + // Black list + {"", "127.0.0.1", "127.0.0.1", "/", "403", false}, + {"", "127.0.0.1", "127.0.0.1", "/dispatch", "403", false}, + {"", "127.0.0.1", "127.0.0.1", "/dump/info", "403", false}, + + {"", "127.0.0.1|/", "127.0.0.1", "/", "403", false}, + {"", "127.0.0.1|/", "127.0.0.1", "/dispatch", "200", false}, + {"", "127.0.0.1|/", "127.0.0.1", "/dump/info", "200", false}, + + {"", "127.0.0.1|/*", "127.0.0.1", "/", "403", false}, + {"", "127.0.0.1|/*", "127.0.0.1", "/dispatch", "403", false}, + {"", "127.0.0.1|/*", "127.0.0.1", "/dump/info", "403", false}, + + {"", "127.0.0.1|/dump/*", "127.0.0.1", "/", "200", false}, + {"", "127.0.0.1|/dump/*", "127.0.0.1", "/dispatch", "200", false}, + {"", "127.0.0.1|/dump/*", "127.0.0.1", "/dump/info", "403", false}, + {"", "127.0.0.1|/dump/*", "127.0.0.1", "/dump/test", "403", false}, + + {"", "127.0.0.1|/dump/info", "127.0.0.1", "/", "200", false}, + {"", "127.0.0.1|/dump/info", "127.0.0.1", "/dispatch", "200", false}, + {"", "127.0.0.1|/dump/info", "127.0.0.1", "/dump/info", "403", false}, + {"", "127.0.0.1|/dump/info", "127.0.0.1", "/dump/test", "200", false}, + + {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/", "200", false}, + {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/dispatch", "200", false}, + {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/dump/info", "403", false}, + {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/dump/test", "403", false}, + {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/dump/fail", "200", false}, + + {"", "127.0.0.0-2|", "127.0.0.1", "/", "403", false}, + {"", "127.0.0.0-2|", "127.0.0.1", "/dump/info", "200", false}, + + {"", "127.0.0.0-2|/", "127.0.0.1", "/", "403", false}, + {"", "127.0.0.0-2|/", "127.0.0.1", "/dispatch", "200", false}, + {"", "127.0.0.0-2|/", "127.0.0.1", "/dump/info", "200", false}, + + {"", "127.0.0.0-2|/dump/*", "127.0.0.1", "/", "200", false}, + {"", "127.0.0.0-2|/dump/*", "127.0.0.1", "/dispatch", "200", false}, + {"", "127.0.0.0-2|/dump/*", "127.0.0.1", "/dump/info", "403", false}, + + {"", "127.0.0.0-2|/dump/info", "127.0.0.1", "/", "200", false}, + {"", "127.0.0.0-2|/dump/info", "127.0.0.1", "/dispatch", "200", false}, + {"", "127.0.0.0-2|/dump/info", "127.0.0.1", "/dump/info", "403", false}, + {"", "127.0.0.0-2|/dump/info", "127.0.0.1", "/dump/test", "200", false}, + + {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/", "200", false}, + {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/dispatch", "200", false}, + {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/dump/info", "403", false}, + {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/dump/test", "403", false}, + {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/dump/fail", "200", false}, + + // Both lists + {"127.0.0.1|/dump", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump", "200", false}, + {"127.0.0.1|/dump", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump/info", "403", false}, + {"127.0.0.1|/dump", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump/fail", "403", false}, + + {"127.0.0.1|/dump/*", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump", "200", false}, + {"127.0.0.1|/dump/*", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump/info", "200", false}, + {"127.0.0.1|/dump/*", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump/fail", "403", false}, + + {"127.0.0.1|/dump/*", "127.0.0.1|/dump/test;127.0.0.1|/dump/fail", "127.0.0.1", "/dump", "200", false}, + {"127.0.0.1|/dump/*", "127.0.0.1|/dump/test;127.0.0.1|/dump/fail", "127.0.0.1", "/dump/info", "200", false}, + {"127.0.0.1|/dump/*", "127.0.0.1|/dump/test;127.0.0.1|/dump/fail", "127.0.0.1", "/dump/test", "403", false}, + {"127.0.0.1|/dump/*", "127.0.0.1|/dump/test;127.0.0.1|/dump/fail", "127.0.0.1", "/dump/fail", "403", false}, + + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1|/dump/test", "127.0.0.1", "/dump", "403", false}, + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1|/dump/test", "127.0.0.1", "/dump/info", "200", false}, + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1|/dump/test", "127.0.0.1", "/dump/test", "403", false}, + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1|/dump/test", "127.0.0.1", "/dump/fail", "403", false}, + + {"127.0.0.1|/;127.0.0.0-2|/dump/*", "127.0.0.0,1|/dump/fail", "127.0.0.1", "/", "200", false}, + {"127.0.0.1|/;127.0.0.0-2|/dump/*", "127.0.0.0,1|/dump/fail", "127.0.0.1", "/dump/info", "200", false}, + {"127.0.0.1|/;127.0.0.0-2|/dump/*", "127.0.0.0,1|/dump/fail", "127.0.0.1", "/dump/fail", "403", false}, + + // Different address + {"127.0.0.2", "", "127.0.0.1", "/", "403", false}, + {"127.0.0.2", "", "127.0.0.1", "/dump/info", "403", false}, + + {"127.0.0.2|/dump/*", "", "127.0.0.1", "/", "403", false}, + {"127.0.0.2|/dump/*", "", "127.0.0.1", "/dump/info", "403", false}, + + {"127.0.0.2|/dump/info", "", "127.0.0.1", "/", "403", false}, + {"127.0.0.2|/dump/info", "", "127.0.0.1", "/dump/info", "403", false}, + {"127.0.0.2|/dump/info", "", "127.0.0.1", "/dump/test", "403", false}, + + {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/", "403", false}, + {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/dispatch", "403", false}, + {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/dump/info", "200", false}, + {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/dump/test", "403", false}, + {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/dump/fail", "403", false}, + + {"172.0.0.0-255", "", "127.0.0.1", "/", "403", false}, + {"172.0.0.0-255", "", "127.0.0.1", "/dump/info", "403", false}, + + {"172.0.0.0-255|/dump/*;127.0.0.0-255|/dump/*", "", "127.0.0.1", "/", "403", false}, + {"172.0.0.0-255|/dump/*;127.0.0.0-255|/dump/*", "", "127.0.0.1", "/dispatch", "403", false}, + {"172.0.0.0-255|/dump/*;127.0.0.0-255|/dump/*", "", "127.0.0.1", "/dump/info", "200", false}, + + /*-----------------------------------------------------------------------------------------*/ + // Match by path starts with [117] + // test cases affected by _whiteListByPath highlighted accordingly + + {"", "", "127.0.0.1", "/", "200", true}, + {"", "", "127.0.0.1", "/dump/info", "200", true}, + + // White list + {"127.0.0.1", "", "127.0.0.1", "/", "200", true}, + {"127.0.0.1", "", "127.0.0.1", "/dispatch", "200", true}, + {"127.0.0.1", "", "127.0.0.1", "/dump/info", "200", true}, + + {"127.0.0.1|/", "", "127.0.0.1", "/", "200", true}, + {"127.0.0.1|/", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath + {"127.0.0.1|/", "", "127.0.0.1", "/dump/info", "200", true}, // _whiteListByPath + + {"127.0.0.1|/*", "", "127.0.0.1", "/", "200", true}, + {"127.0.0.1|/*", "", "127.0.0.1", "/dispatch", "200", true}, + {"127.0.0.1|/*", "", "127.0.0.1", "/dump/info", "200", true}, + + {"127.0.0.1|/dump/*", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath + {"127.0.0.1|/dump/*", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath + {"127.0.0.1|/dump/*", "", "127.0.0.1", "/dump/info", "200", true}, + {"127.0.0.1|/dump/*", "", "127.0.0.1", "/dump/test", "200", true}, + + {"127.0.0.1|/dump/info", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath + {"127.0.0.1|/dump/info", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath + {"127.0.0.1|/dump/info", "", "127.0.0.1", "/dump/info", "200", true}, + {"127.0.0.1|/dump/info", "", "127.0.0.1", "/dump/test", "200", true}, // _whiteListByPath + + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/dump/info", "200", true}, + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/dump/test", "200", true}, + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "", "127.0.0.1", "/dump/fail", "200", true}, // _whiteListByPath + + {"127.0.0.0-2|", "", "127.0.0.1", "/", "200", true}, + {"127.0.0.0-2|", "", "127.0.0.1", "/dump/info", "200", true}, + + {"127.0.0.0-2|/", "", "127.0.0.1", "/", "200", true}, + {"127.0.0.0-2|/", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath + {"127.0.0.0-2|/", "", "127.0.0.1", "/dump/info", "200", true}, // _whiteListByPath + + {"127.0.0.0-2|/dump/*", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath + {"127.0.0.0-2|/dump/*", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath + {"127.0.0.0-2|/dump/*", "", "127.0.0.1", "/dump/info", "200", true}, + + {"127.0.0.0-2|/dump/info", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath + {"127.0.0.0-2|/dump/info", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath + {"127.0.0.0-2|/dump/info", "", "127.0.0.1", "/dump/info", "200", true}, + {"127.0.0.0-2|/dump/info", "", "127.0.0.1", "/dump/test", "200", true}, // _whiteListByPath + + {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath + {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath + {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/dump/info", "200", true}, + {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/dump/test", "200", true}, + {"127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "", "127.0.0.1", "/dump/fail", "200", true}, // _whiteListByPath + + // Black list + {"", "127.0.0.1", "127.0.0.1", "/", "403", true}, + {"", "127.0.0.1", "127.0.0.1", "/dispatch", "403", true}, + {"", "127.0.0.1", "127.0.0.1", "/dump/info", "403", true}, + + {"", "127.0.0.1|/", "127.0.0.1", "/", "403", true}, + {"", "127.0.0.1|/", "127.0.0.1", "/dispatch", "200", true}, + {"", "127.0.0.1|/", "127.0.0.1", "/dump/info", "200", true}, + + {"", "127.0.0.1|/*", "127.0.0.1", "/", "403", true}, + {"", "127.0.0.1|/*", "127.0.0.1", "/dispatch", "403", true}, + {"", "127.0.0.1|/*", "127.0.0.1", "/dump/info", "403", true}, + + {"", "127.0.0.1|/dump/*", "127.0.0.1", "/", "200", true}, + {"", "127.0.0.1|/dump/*", "127.0.0.1", "/dispatch", "200", true}, + {"", "127.0.0.1|/dump/*", "127.0.0.1", "/dump/info", "403", true}, + {"", "127.0.0.1|/dump/*", "127.0.0.1", "/dump/test", "403", true}, + + {"", "127.0.0.1|/dump/info", "127.0.0.1", "/", "200", true}, + {"", "127.0.0.1|/dump/info", "127.0.0.1", "/dispatch", "200", true}, + {"", "127.0.0.1|/dump/info", "127.0.0.1", "/dump/info", "403", true}, + {"", "127.0.0.1|/dump/info", "127.0.0.1", "/dump/test", "200", true}, + + {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/", "200", true}, + {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/dispatch", "200", true}, + {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/dump/info", "403", true}, + {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/dump/test", "403", true}, + {"", "127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1", "/dump/fail", "200", true}, + + {"", "127.0.0.0-2|", "127.0.0.1", "/", "403", true}, + {"", "127.0.0.0-2|", "127.0.0.1", "/dump/info", "200", true}, + + {"", "127.0.0.0-2|/", "127.0.0.1", "/", "403", true}, + {"", "127.0.0.0-2|/", "127.0.0.1", "/dispatch", "200", true}, + {"", "127.0.0.0-2|/", "127.0.0.1", "/dump/info", "200", true}, + + {"", "127.0.0.0-2|/dump/*", "127.0.0.1", "/", "200", true}, + {"", "127.0.0.0-2|/dump/*", "127.0.0.1", "/dispatch", "200", true}, + {"", "127.0.0.0-2|/dump/*", "127.0.0.1", "/dump/info", "403", true}, + + {"", "127.0.0.0-2|/dump/info", "127.0.0.1", "/", "200", true}, + {"", "127.0.0.0-2|/dump/info", "127.0.0.1", "/dispatch", "200", true}, + {"", "127.0.0.0-2|/dump/info", "127.0.0.1", "/dump/info", "403", true}, + {"", "127.0.0.0-2|/dump/info", "127.0.0.1", "/dump/test", "200", true}, + + {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/", "200", true}, + {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/dispatch", "200", true}, + {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/dump/info", "403", true}, + {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/dump/test", "403", true}, + {"", "127.0.0.0-2|/dump/info;127.0.0.0-2|/dump/test", "127.0.0.1", "/dump/fail", "200", true}, + + // Both lists + {"127.0.0.1|/dump", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump", "200", true}, + {"127.0.0.1|/dump", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump/info", "200", true}, // _whiteListByPath + {"127.0.0.1|/dump", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump/fail", "403", true}, + + {"127.0.0.1|/dump/*", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump", "200", true}, + {"127.0.0.1|/dump/*", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump/info", "200", true}, + {"127.0.0.1|/dump/*", "127.0.0.1|/dump/fail", "127.0.0.1", "/dump/fail", "403", true}, + + {"127.0.0.1|/dump/*", "127.0.0.1|/dump/test;127.0.0.1|/dump/fail", "127.0.0.1", "/dump", "200", true}, + {"127.0.0.1|/dump/*", "127.0.0.1|/dump/test;127.0.0.1|/dump/fail", "127.0.0.1", "/dump/info", "200", true}, + {"127.0.0.1|/dump/*", "127.0.0.1|/dump/test;127.0.0.1|/dump/fail", "127.0.0.1", "/dump/test", "403", true}, + {"127.0.0.1|/dump/*", "127.0.0.1|/dump/test;127.0.0.1|/dump/fail", "127.0.0.1", "/dump/fail", "403", true}, + + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1|/dump/test", "127.0.0.1", "/dump", "200", true}, // _whiteListByPath + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1|/dump/test", "127.0.0.1", "/dump/info", "200", true}, + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1|/dump/test", "127.0.0.1", "/dump/test", "403", true}, + {"127.0.0.1|/dump/info;127.0.0.1|/dump/test", "127.0.0.1|/dump/test", "127.0.0.1", "/dump/fail", "200", true}, // _whiteListByPath + + {"127.0.0.1|/;127.0.0.0-2|/dump/*", "127.0.0.0,1|/dump/fail", "127.0.0.1", "/", "200", true}, + {"127.0.0.1|/;127.0.0.0-2|/dump/*", "127.0.0.0,1|/dump/fail", "127.0.0.1", "/dump/info", "200", true}, + {"127.0.0.1|/;127.0.0.0-2|/dump/*", "127.0.0.0,1|/dump/fail", "127.0.0.1", "/dump/fail", "403", true}, + + // Different address + {"127.0.0.2", "", "127.0.0.1", "/", "403", true}, + {"127.0.0.2", "", "127.0.0.1", "/dump/info", "403", true}, + + {"127.0.0.2|/dump/*", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath + {"127.0.0.2|/dump/*", "", "127.0.0.1", "/dump/info", "403", true}, + + {"127.0.0.2|/dump/info", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath + {"127.0.0.2|/dump/info", "", "127.0.0.1", "/dump/info", "403", true}, + {"127.0.0.2|/dump/info", "", "127.0.0.1", "/dump/test", "200", true}, // _whiteListByPath + + {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath + {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath + {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/dump/info", "200", true}, + {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/dump/test", "403", true}, + {"127.0.0.1|/dump/info;127.0.0.2|/dump/test", "", "127.0.0.1", "/dump/fail", "200", true}, // _whiteListByPath + + {"172.0.0.0-255", "", "127.0.0.1", "/", "403", true}, + {"172.0.0.0-255", "", "127.0.0.1", "/dump/info", "403", true}, + + {"172.0.0.0-255|/dump/*;127.0.0.0-255|/dump/*", "", "127.0.0.1", "/", "200", true}, // _whiteListByPath + {"172.0.0.0-255|/dump/*;127.0.0.0-255|/dump/*", "", "127.0.0.1", "/dispatch", "200", true}, // _whiteListByPath + {"172.0.0.0-255|/dump/*;127.0.0.0-255|/dump/*", "", "127.0.0.1", "/dump/info", "200", true}, + }; + return Arrays.asList(data).stream().map(Arguments::of); + } +} diff --git a/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppContextTest.java b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppContextTest.java index a657670370d..2334041fc7e 100644 --- a/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppContextTest.java +++ b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppContextTest.java @@ -18,8 +18,14 @@ package org.eclipse.jetty.webapp; +<<<<<<< HEAD import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +======= import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -27,7 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; - +>>>>>>> a3f1592c50... Issue #2431 - Upgrade to Junit 5 (#2436) import java.io.File; import java.io.IOException; @@ -113,8 +119,7 @@ public class WebAppContextTest //test if no classnames set, its the defaults WebAppContext wac = new WebAppContext(); - assertThat(wac.getWebAppConfigurations().stream().map(c->{return c.getClass().getName();}).collect(Collectors.toList()), - containsInAnyOrder(known_and_enabled)); + assertThat(wac.getWebAppConfigurations().stream().map(c->{return c.getClass().getName();}).collect(Collectors.toList()),Matchers.containsInAnyOrder(known_and_enabled)); String[] classNames = wac.getConfigurationClasses(); assertNotNull(classNames); @@ -128,8 +133,8 @@ public class WebAppContextTest { WebAppContext wac = new WebAppContext(); wac.setServer(new Server()); - assertThat(wac.getWebAppConfigurations().stream().map(c->c.getClass().getName()).collect(Collectors.toList()), - contains( + Assert.assertThat(wac.getWebAppConfigurations().stream().map(c->c.getClass().getName()).collect(Collectors.toList()), + Matchers.contains( "org.eclipse.jetty.webapp.JmxConfiguration", "org.eclipse.jetty.webapp.WebInfConfiguration", "org.eclipse.jetty.webapp.WebXmlConfiguration", @@ -145,14 +150,14 @@ public class WebAppContextTest Configuration[] configs = {new WebInfConfiguration()}; WebAppContext wac = new WebAppContext(); wac.setConfigurations(configs); - assertThat(wac.getWebAppConfigurations(),contains(configs)); + Assert.assertThat(wac.getWebAppConfigurations(),Matchers.contains(configs)); //test that explicit config instances override any from server String[] classNames = {"x.y.z"}; Server server = new Server(); server.setAttribute(Configuration.ATTR, classNames); wac.setServer(server); - assertThat(wac.getWebAppConfigurations(),contains(configs)); + Assert.assertThat(wac.getWebAppConfigurations(),Matchers.contains(configs)); } @Test @@ -259,8 +264,11 @@ public class WebAppContextTest try { String response = connector.getResponse("GET http://localhost:8080 HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n"); +<<<<<<< HEAD assertThat(response,containsString("200 OK")); - +======= + assertTrue(response.indexOf("200 OK")>=0); +>>>>>>> a3f1592c50... Issue #2431 - Upgrade to Junit 5 (#2436) } finally { diff --git a/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebInfConfigurationTest.java b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebInfConfigurationTest.java new file mode 100644 index 00000000000..21634e101a0 --- /dev/null +++ b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebInfConfigurationTest.java @@ -0,0 +1,121 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 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.webapp; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.endsWith; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import org.eclipse.jetty.util.JavaVersion; +import org.eclipse.jetty.util.resource.Resource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnJre; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.junit.jupiter.api.condition.EnabledOnJre; +import org.junit.jupiter.api.condition.JRE; + +/** + * WebInfConfigurationTest + * + * + */ +public class WebInfConfigurationTest +{ + + /** + * Assume target < jdk9. In this case, we should be able to extract + * the urls from the application classloader, and we should not look + * at the java.class.path property. + * @throws Exception + */ + @Test + @EnabledOnJre(JRE.JAVA_8) + public void testFindAndFilterContainerPaths() + throws Exception + { + WebInfConfiguration config = new WebInfConfiguration(); + WebAppContext context = new WebAppContext(); + context.setAttribute(WebInfConfiguration.CONTAINER_JAR_PATTERN, ".*/jetty-util-[^/]*\\.jar$|.*/jetty-util/target/classes/"); + + WebAppClassLoader loader = new WebAppClassLoader(context); + context.setClassLoader(loader); + config.findAndFilterContainerPaths(context); + List containerResources = context.getMetaData().getContainerResources(); + assertEquals(1, containerResources.size()); + assertThat(containerResources.get(0).toString(), containsString("jetty-util")); + } + + /** + * Assume target jdk9 or above. In this case we should extract what we need + * from the java.class.path. We should also examine the module path. + * @throws Exception + */ + @Test + @DisabledOnJre(JRE.JAVA_8) + @EnabledIfSystemProperty(named="jdk.module.path", matches=".*") + public void testFindAndFilterContainerPathsJDK9() + throws Exception + { + WebInfConfiguration config = new WebInfConfiguration(); + WebAppContext context = new WebAppContext(); + context.setAttribute(WebInfConfiguration.CONTAINER_JAR_PATTERN, ".*/jetty-util-[^/]*\\.jar$|.*/jetty-util/target/classes/$|.*/foo-bar-janb.jar"); + WebAppClassLoader loader = new WebAppClassLoader(context); + context.setClassLoader(loader); + config.findAndFilterContainerPaths(context); + List containerResources = context.getMetaData().getContainerResources(); + assertEquals(2, containerResources.size()); + for (Resource r:containerResources) + { + String s = r.toString(); + assertThat(s, anyOf(endsWith("foo-bar-janb.jar"), containsString("jetty-util"))); + } + } + + + /** + * Assume runtime is jdk9 or above. Target is jdk 8. In this + * case we must extract from the java.class.path (because jdk 9 + * has no url based application classloader), but we should + * ignore the module path. + * @throws Exception + */ + @Test + @DisabledOnJre(JRE.JAVA_8) + @EnabledIfSystemProperty(named="jdk.module.path", matches=".*") + public void testFindAndFilterContainerPathsTarget8() + throws Exception + { + WebInfConfiguration config = new WebInfConfiguration(); + WebAppContext context = new WebAppContext(); + context.setAttribute(JavaVersion.JAVA_TARGET_PLATFORM, "8"); + context.setAttribute(WebInfConfiguration.CONTAINER_JAR_PATTERN, ".*/jetty-util-[^/]*\\.jar$|.*/jetty-util/target/classes/$|.*/foo-bar-janb.jar"); + WebAppClassLoader loader = new WebAppClassLoader(context); + context.setClassLoader(loader); + config.findAndFilterContainerPaths(context); + List containerResources = context.getMetaData().getContainerResources(); + assertEquals(1, containerResources.size()); + assertThat(containerResources.get(0).toString(), containsString("jetty-util")); + } + +} diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ClientCloseTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ClientCloseTest.java new file mode 100644 index 00000000000..e95cbd4f3c7 --- /dev/null +++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ClientCloseTest.java @@ -0,0 +1,673 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 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.client; + + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; + +import static java.time.Duration.ofSeconds; +import static org.hamcrest.MatcherAssert.assertThat; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + + +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.io.ManagedSelector; +import org.eclipse.jetty.io.SelectorManager; +import org.eclipse.jetty.io.SocketChannelEndPoint; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.log.StacklessLogging; +import org.eclipse.jetty.util.thread.Scheduler; +import org.eclipse.jetty.websocket.api.ProtocolException; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.StatusCode; +import org.eclipse.jetty.websocket.api.WebSocketAdapter; +import org.eclipse.jetty.websocket.common.CloseInfo; +import org.eclipse.jetty.websocket.common.OpCode; +import org.eclipse.jetty.websocket.common.Parser; +import org.eclipse.jetty.websocket.common.WebSocketFrame; +import org.eclipse.jetty.websocket.common.WebSocketSession; +import org.eclipse.jetty.websocket.common.frames.TextFrame; +import org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection; +import org.eclipse.jetty.websocket.common.test.BlockheadConnection; +import org.eclipse.jetty.websocket.common.test.BlockheadServer; +import org.eclipse.jetty.websocket.common.test.RawFrameBuilder; +import org.eclipse.jetty.websocket.common.test.Timeouts; +import org.hamcrest.Matcher; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + + +public class ClientCloseTest +{ + private static final Logger LOG = Log.getLogger(ClientCloseTest.class); + + private static class CloseTrackingSocket extends WebSocketAdapter + { + private static final Logger LOG = ClientCloseTest.LOG.getLogger("CloseTrackingSocket"); + + public int closeCode = -1; + public String closeReason = null; + public CountDownLatch closeLatch = new CountDownLatch(1); + public AtomicInteger closeCount = new AtomicInteger(0); + public CountDownLatch openLatch = new CountDownLatch(1); + public CountDownLatch errorLatch = new CountDownLatch(1); + + public LinkedBlockingQueue messageQueue = new LinkedBlockingQueue<>(); + public AtomicReference error = new AtomicReference<>(); + + public void assertNoCloseEvent() + { + assertThat("Client Close Event",closeLatch.getCount(),is(1L)); + assertThat("Client Close Event Status Code ",closeCode,is(-1)); + } + + public void assertReceivedCloseEvent(int clientTimeoutMs, Matcher statusCodeMatcher, Matcher reasonMatcher) + throws InterruptedException + { + long maxTimeout = clientTimeoutMs * 4; + + assertThat("Client Close Event Occurred",closeLatch.await(maxTimeout,TimeUnit.MILLISECONDS),is(true)); + assertThat("Client Close Event Count",closeCount.get(),is(1)); + assertThat("Client Close Event Status Code",closeCode,statusCodeMatcher); + if (reasonMatcher == null) + { + assertThat("Client Close Event Reason",closeReason,nullValue()); + } + else + { + assertThat("Client Close Event Reason",closeReason,reasonMatcher); + } + } + + public void clearQueues() + { + messageQueue.clear(); + } + + @Override + public void onWebSocketClose(int statusCode, String reason) + { + LOG.debug("onWebSocketClose({},{})",statusCode,reason); + super.onWebSocketClose(statusCode,reason); + closeCount.incrementAndGet(); + closeCode = statusCode; + closeReason = reason; + closeLatch.countDown(); + } + + @Override + public void onWebSocketConnect(Session session) + { + LOG.debug("onWebSocketConnect({})",session); + super.onWebSocketConnect(session); + openLatch.countDown(); + } + + @Override + public void onWebSocketError(Throwable cause) + { + LOG.debug("onWebSocketError",cause); + assertThat("Unique Error Event", error.compareAndSet(null, cause), is(true)); + errorLatch.countDown(); + } + + @Override + public void onWebSocketText(String message) + { + LOG.debug("onWebSocketText({})",message); + messageQueue.offer(message); + } + + public EndPoint getEndPoint() throws Exception + { + Session session = getSession(); + assertThat("Session type",session,instanceOf(WebSocketSession.class)); + + WebSocketSession wssession = (WebSocketSession)session; + Field fld = wssession.getClass().getDeclaredField("connection"); + fld.setAccessible(true); + assertThat("Field: connection",fld,notNullValue()); + + Object val = fld.get(wssession); + assertThat("Connection type",val,instanceOf(AbstractWebSocketConnection.class)); + @SuppressWarnings("resource") + AbstractWebSocketConnection wsconn = (AbstractWebSocketConnection)val; + return wsconn.getEndPoint(); + } + } + + private static BlockheadServer server; + private WebSocketClient client; + + private void confirmConnection(CloseTrackingSocket clientSocket, Future clientFuture, BlockheadConnection serverConns) throws Exception + { + // Wait for client connect on via future + clientFuture.get(30,TimeUnit.SECONDS); + + // Wait for client connect via client websocket + assertThat("Client WebSocket is Open",clientSocket.openLatch.await(30,TimeUnit.SECONDS),is(true)); + + try + { + // Send message from client to server + final String echoMsg = "echo-test"; + Future testFut = clientSocket.getRemote().sendStringByFuture(echoMsg); + + // Wait for send future + testFut.get(Timeouts.SEND, Timeouts.SEND_UNIT); + + // Read Frame on server side + LinkedBlockingQueue serverCapture = serverConns.getFrameQueue(); + WebSocketFrame frame = serverCapture.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT); + assertThat("Server received frame",frame.getOpCode(),is(OpCode.TEXT)); + assertThat("Server received frame payload",frame.getPayloadAsUTF8(),is(echoMsg)); + + // Server send echo reply + serverConns.write(new TextFrame().setPayload(echoMsg)); + + // Verify received message + String recvMsg = clientSocket.messageQueue.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT); + assertThat("Received message",recvMsg,is(echoMsg)); + + // Verify that there are no errors + assertThat("Error events",clientSocket.error.get(),nullValue()); + } + finally + { + clientSocket.clearQueues(); + } + } + + private void confirmServerReceivedCloseFrame(BlockheadConnection serverConn, int expectedCloseCode, Matcher closeReasonMatcher) throws InterruptedException + { + LinkedBlockingQueue serverCapture = serverConn.getFrameQueue(); + WebSocketFrame frame = serverCapture.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT); + assertThat("Server close frame", frame, is(notNullValue())); + assertThat("Server received close frame",frame.getOpCode(),is(OpCode.CLOSE)); + CloseInfo closeInfo = new CloseInfo(frame); + assertThat("Server received close code",closeInfo.getStatusCode(),is(expectedCloseCode)); + if (closeReasonMatcher == null) + { + assertThat("Server received close reason",closeInfo.getReason(),nullValue()); + } + else + { + assertThat("Server received close reason",closeInfo.getReason(),closeReasonMatcher); + } + } + + public static class TestClientTransportOverHTTP extends HttpClientTransportOverHTTP + { + @Override + protected SelectorManager newSelectorManager(HttpClient client) + { + return new ClientSelectorManager(client, 1){ + @Override + protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key) + { + TestEndPoint endPoint = new TestEndPoint(channel,selector,key,getScheduler()); + endPoint.setIdleTimeout(client.getIdleTimeout()); + return endPoint; + } + }; + } + } + + public static class TestEndPoint extends SocketChannelEndPoint + { + public AtomicBoolean congestedFlush = new AtomicBoolean(false); + + public TestEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler) + { + super((SocketChannel)channel,selector,key,scheduler); + } + + @Override + public boolean flush(ByteBuffer... buffers) throws IOException + { + boolean flushed = super.flush(buffers); + congestedFlush.set(!flushed); + return flushed; + } + } + + @BeforeEach + public void startClient() throws Exception + { + HttpClient httpClient = new HttpClient(new TestClientTransportOverHTTP(), null); + client = new WebSocketClient(httpClient); + client.addBean(httpClient); + client.start(); + } + + @BeforeAll + public static void startServer() throws Exception + { + server = new BlockheadServer(); + server.start(); + } + + @AfterEach + public void stopClient() throws Exception + { + client.stop(); + } + + @AfterAll + public static void stopServer() throws Exception + { + server.stop(); + } + + @Test + public void testHalfClose() throws Exception + { + // Set client timeout + final int timeout = 5000; + client.setMaxIdleTimeout(timeout); + + // Hook into server connection creation + CompletableFuture serverConnFut = new CompletableFuture<>(); + server.addConnectFuture(serverConnFut); + + // Client connects + CloseTrackingSocket clientSocket = new CloseTrackingSocket(); + Future clientConnectFuture = client.connect(clientSocket,server.getWsUri()); + + try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) + { + // client confirms connection via echo + confirmConnection(clientSocket, clientConnectFuture, serverConn); + + // client sends close frame (code 1000, normal) + final String origCloseReason = "Normal Close"; + clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason); + + // server receives close frame + confirmServerReceivedCloseFrame(serverConn, StatusCode.NORMAL, is(origCloseReason)); + + // server sends 2 messages + serverConn.write(new TextFrame().setPayload("Hello")); + serverConn.write(new TextFrame().setPayload("World")); + + // server sends close frame (code 1000, no reason) + CloseInfo sclose = new CloseInfo(StatusCode.NORMAL, "From Server"); + serverConn.write(sclose.asFrame()); + + // Verify received messages + String recvMsg = clientSocket.messageQueue.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT); + assertThat("Received message 1", recvMsg, is("Hello")); + recvMsg = clientSocket.messageQueue.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT); + assertThat("Received message 2", recvMsg, is("World")); + + // Verify that there are no errors + assertThat("Error events", clientSocket.error.get(), nullValue()); + + // client close event on ws-endpoint + clientSocket.assertReceivedCloseEvent(timeout, is(StatusCode.NORMAL), containsString("From Server")); + } + + assertThat("Client Open Sessions", client.getOpenSessions(), empty()); + } + + @Disabled("Need sbordet's help here") + @Test + public void testNetworkCongestion() throws Exception + { + // Set client timeout + final int timeout = 1000; + client.setMaxIdleTimeout(timeout); + + // Hook into server connection creation + CompletableFuture serverConnFut = new CompletableFuture<>(); + server.addConnectFuture(serverConnFut); + + // Client connects + CloseTrackingSocket clientSocket = new CloseTrackingSocket(); + Future clientConnectFuture = client.connect(clientSocket,server.getWsUri()); + + try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) + { + // client confirms connection via echo + confirmConnection(clientSocket, clientConnectFuture, serverConn); + + // client sends BIG frames (until it cannot write anymore) + // server must not read (for test purpose, in order to congest connection) + // when write is congested, client enqueue close frame + // client initiate write, but write never completes + EndPoint endp = clientSocket.getEndPoint(); + assertThat("EndPoint is testable", endp, instanceOf(TestEndPoint.class)); + TestEndPoint testendp = (TestEndPoint) endp; + + char msg[] = new char[10240]; + int writeCount = 0; + long writeSize = 0; + int i = 0; + while (!testendp.congestedFlush.get()) + { + int z = i - ((i / 26) * 26); + char c = (char) ('a' + z); + Arrays.fill(msg, c); + clientSocket.getRemote().sendStringByFuture(String.valueOf(msg)); + writeCount++; + writeSize += msg.length; + } + LOG.info("Wrote {} frames totalling {} bytes of payload before congestion kicked in", writeCount, writeSize); + + // Verify timeout error + assertThat("OnError Latch", clientSocket.errorLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat("OnError", clientSocket.error.get(), instanceOf(SocketTimeoutException.class)); + } + } + + @Test + public void testProtocolException() throws Exception + { + // Set client timeout + final int timeout = 1000; + client.setMaxIdleTimeout(timeout); + + // Hook into server connection creation + CompletableFuture serverConnFut = new CompletableFuture<>(); + server.addConnectFuture(serverConnFut); + + // Client connects + CloseTrackingSocket clientSocket = new CloseTrackingSocket(); + Future clientConnectFuture = client.connect(clientSocket,server.getWsUri()); + + try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) + { + // client confirms connection via echo + confirmConnection(clientSocket, clientConnectFuture, serverConn); + + // client should not have received close message (yet) + clientSocket.assertNoCloseEvent(); + + // server sends bad close frame (too big of a reason message) + byte msg[] = new byte[400]; + Arrays.fill(msg, (byte) 'x'); + ByteBuffer bad = ByteBuffer.allocate(500); + RawFrameBuilder.putOpFin(bad, OpCode.CLOSE, true); + RawFrameBuilder.putLength(bad, msg.length + 2, false); + bad.putShort((short) StatusCode.NORMAL); + bad.put(msg); + BufferUtil.flipToFlush(bad, 0); + + try (StacklessLogging ignore = new StacklessLogging(Parser.class)) + { + serverConn.writeRaw(bad); + + // client should have noticed the error + assertThat("OnError Latch", clientSocket.errorLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat("OnError", clientSocket.error.get(), instanceOf(ProtocolException.class)); + assertThat("OnError", clientSocket.error.get().getMessage(), containsString("Invalid control frame")); + + // client parse invalid frame, notifies server of close (protocol error) + confirmServerReceivedCloseFrame(serverConn, StatusCode.PROTOCOL, allOf(containsString("Invalid control frame"), containsString("length"))); + } + } + + // client triggers close event on client ws-endpoint + clientSocket.assertReceivedCloseEvent(timeout,is(StatusCode.PROTOCOL),allOf(containsString("Invalid control frame"),containsString("length"))); + assertThat("Client Open Sessions", client.getOpenSessions(), empty()); + } + + @Test + public void testReadEOF() throws Exception + { + // Set client timeout + final int timeout = 1000; + client.setMaxIdleTimeout(timeout); + + // Hook into server connection creation + CompletableFuture serverConnFut = new CompletableFuture<>(); + server.addConnectFuture(serverConnFut); + + // Client connects + CloseTrackingSocket clientSocket = new CloseTrackingSocket(); + Future clientConnectFuture = client.connect(clientSocket,server.getWsUri()); + + try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) + { + // client confirms connection via echo + confirmConnection(clientSocket, clientConnectFuture, serverConn); + + // client sends close frame + final String origCloseReason = "Normal Close"; + clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason); + + // server receives close frame + confirmServerReceivedCloseFrame(serverConn, StatusCode.NORMAL, is(origCloseReason)); + + // client should not have received close message (yet) + clientSocket.assertNoCloseEvent(); + + // server shuts down connection (no frame reply) + serverConn.abort(); + + // client reads -1 (EOF) + // client triggers close event on client ws-endpoint + clientSocket.assertReceivedCloseEvent(timeout, is(StatusCode.ABNORMAL), + anyOf( + containsString("EOF"), + containsString("Disconnected") + )); + } + assertThat("Client Open Sessions", client.getOpenSessions(), empty()); + } + + @Test + public void testServerNoCloseHandshake() throws Exception + { + // Set client timeout + final int timeout = 1000; + client.setMaxIdleTimeout(timeout); + + // Hook into server connection creation + CompletableFuture serverConnFut = new CompletableFuture<>(); + server.addConnectFuture(serverConnFut); + + // Client connects + CloseTrackingSocket clientSocket = new CloseTrackingSocket(); + Future clientConnectFuture = client.connect(clientSocket,server.getWsUri()); + + try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) + { + // client confirms connection via echo + confirmConnection(clientSocket, clientConnectFuture, serverConn); + + // client sends close frame + final String origCloseReason = "Normal Close"; + clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason); + + // server receives close frame + confirmServerReceivedCloseFrame(serverConn, StatusCode.NORMAL, is(origCloseReason)); + + // client should not have received close message (yet) + clientSocket.assertNoCloseEvent(); + + // server never sends close frame handshake + // server sits idle + + // client idle timeout triggers close event on client ws-endpoint + assertThat("OnError Latch", clientSocket.errorLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat("OnError", clientSocket.error.get(), instanceOf(TimeoutException.class)); + + // client close should occur + clientSocket.assertReceivedCloseEvent(timeout, is(StatusCode.ABNORMAL), + anyOf( + containsString("Timeout"), + containsString("Disconnected") + )); + } + assertThat("Client Open Sessions", client.getOpenSessions(), empty()); + } + + @Test + public void testStopLifecycle() throws Exception + { + // Set client timeout + final int timeout = 1000; + client.setMaxIdleTimeout(timeout); + + int clientCount = 3; + List clientSockets = new ArrayList<>(); + List> serverConnFuts = new ArrayList<>(); + List serverConns = new ArrayList<>(); + + try + { + assertTimeoutPreemptively(ofSeconds(5), ()-> { + // Open Multiple Clients + for (int i = 0; i < clientCount; i++) + { + // Client Request Upgrade + CloseTrackingSocket clientSocket = new CloseTrackingSocket(); + clientSockets.add(clientSocket); + Future clientConnectFuture = client.connect(clientSocket, server.getWsUri()); + + // Server accepts connection + CompletableFuture serverConnFut = new CompletableFuture<>(); + serverConnFuts.add(serverConnFut); + server.addConnectFuture(serverConnFut); + BlockheadConnection serverConn = serverConnFut.get(); + serverConns.add(serverConn); + + // client confirms connection via echo + confirmConnection(clientSocket, clientConnectFuture, serverConn); + } + + // client lifecycle stop (the meat of this test) + client.stop(); + + // clients send close frames (code 1001, shutdown) + for (int i = 0; i < clientCount; i++) + { + // server receives close frame + confirmServerReceivedCloseFrame(serverConns.get(i), StatusCode.SHUTDOWN, containsString("Shutdown")); + } + + // clients disconnect + for (int i = 0; i < clientCount; i++) + { + clientSockets.get(i).assertReceivedCloseEvent(timeout, is(StatusCode.SHUTDOWN), containsString("Shutdown")); + } + assertThat("Client Open Sessions", client.getOpenSessions(), empty()); + + // clients disconnect + for (int i = 0; i < clientCount; i++) + { + clientSockets.get(i).assertReceivedCloseEvent(timeout, is(StatusCode.SHUTDOWN), containsString("Shutdown")); + } + }); + + } + finally + { + for(BlockheadConnection serverConn: serverConns) + { + try + { + serverConn.close(); + } + catch (Exception ignore) + { + } + } + } + } + + @Test + public void testWriteException() throws Exception + { + // Set client timeout + final int timeout = 1000; + client.setMaxIdleTimeout(timeout); + + // Hook into server connection creation + CompletableFuture serverConnFut = new CompletableFuture<>(); + server.addConnectFuture(serverConnFut); + + // Client connects + CloseTrackingSocket clientSocket = new CloseTrackingSocket(); + Future clientConnectFuture = client.connect(clientSocket,server.getWsUri()); + + try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) + { + // client confirms connection via echo + confirmConnection(clientSocket, clientConnectFuture, serverConn); + + // setup client endpoint for write failure (test only) + EndPoint endp = clientSocket.getEndPoint(); + endp.shutdownOutput(); + + // client enqueue close frame + // client write failure + final String origCloseReason = "Normal Close"; + clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason); + + assertThat("OnError Latch", clientSocket.errorLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat("OnError", clientSocket.error.get(), instanceOf(EofException.class)); + + // client triggers close event on client ws-endpoint + // assert - close code==1006 (abnormal) + // assert - close reason message contains (write failure) + clientSocket.assertReceivedCloseEvent(timeout, is(StatusCode.ABNORMAL), containsString("EOF")); + } + assertThat("Client Open Sessions", client.getOpenSessions(), empty()); + } +} diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ClientConnectTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ClientConnectTest.java new file mode 100644 index 00000000000..14f894a40ef --- /dev/null +++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ClientConnectTest.java @@ -0,0 +1,463 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 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.client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.MappedByteBufferPool; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.UpgradeException; +import org.eclipse.jetty.websocket.common.AcceptHash; +import org.eclipse.jetty.websocket.common.test.BlockheadConnection; +import org.eclipse.jetty.websocket.common.test.BlockheadServer; +import org.eclipse.jetty.websocket.common.test.Timeouts; +import org.hamcrest.Matcher; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Various connect condition testing + */ +@SuppressWarnings("Duplicates") +public class ClientConnectTest +{ + public ByteBufferPool bufferPool = new MappedByteBufferPool(); + + private static BlockheadServer server; + private WebSocketClient client; + + @SuppressWarnings("unchecked") + private E assertExpectedError(ExecutionException e, JettyTrackingSocket wsocket, Matcher errorMatcher) + { + // Validate thrown cause + Throwable cause = e.getCause(); + + assertThat("ExecutionException.cause",cause,errorMatcher); + + // Validate websocket captured cause + assertThat("Error Queue Length",wsocket.errorQueue.size(),greaterThanOrEqualTo(1)); + Throwable capcause = wsocket.errorQueue.poll(); + assertThat("Error Queue[0]",capcause,notNullValue()); + assertThat("Error Queue[0]",capcause,errorMatcher); + + // Validate that websocket didn't see an open event + wsocket.assertNotOpened(); + + // Return the captured cause + return (E)capcause; + } + + @BeforeEach + public void startClient() throws Exception + { + client = new WebSocketClient(); + client.setBufferPool(bufferPool); + client.setConnectTimeout(Timeouts.CONNECT_UNIT.toMillis(Timeouts.CONNECT)); + client.start(); + } + + @BeforeAll + public static void startServer() throws Exception + { + server = new BlockheadServer(); + server.start(); + } + + @BeforeEach + public void resetServerHandler() + { + // for each test, reset the server request handling to default + server.resetRequestHandling(); + } + + @AfterEach + public void stopClient() throws Exception + { + client.stop(); + } + + @AfterAll + public static void stopServer() throws Exception + { + server.stop(); + } + + @Test + public void testUpgradeRequest() throws Exception + { + JettyTrackingSocket wsocket = new JettyTrackingSocket(); + + URI wsUri = server.getWsUri(); + Future future = client.connect(wsocket,wsUri); + + Session sess = future.get(30,TimeUnit.SECONDS); + + wsocket.waitForConnected(); + + assertThat("Connect.UpgradeRequest", wsocket.connectUpgradeRequest, notNullValue()); + assertThat("Connect.UpgradeResponse", wsocket.connectUpgradeResponse, notNullValue()); + + sess.close(); + } + + @Test + public void testAltConnect() throws Exception + { + JettyTrackingSocket wsocket = new JettyTrackingSocket(); + URI wsUri = server.getWsUri(); + + HttpClient httpClient = new HttpClient(); + try + { + httpClient.start(); + + WebSocketUpgradeRequest req = new WebSocketUpgradeRequest(new WebSocketClient(), httpClient, wsUri, wsocket); + req.header("X-Foo", "Req"); + CompletableFuture sess = req.sendAsync(); + + sess.thenAccept((s) -> { + System.out.printf("Session: %s%n", s); + s.close(); + assertThat("Connect.UpgradeRequest", wsocket.connectUpgradeRequest, notNullValue()); + assertThat("Connect.UpgradeResponse", wsocket.connectUpgradeResponse, notNullValue()); + }); + } + finally + { + httpClient.stop(); + } + } + + @Test + public void testUpgradeWithAuthorizationHeader() throws Exception + { + JettyTrackingSocket wsocket = new JettyTrackingSocket(); + + // Hook into server connection creation + CompletableFuture serverConnFut = new CompletableFuture<>(); + server.addConnectFuture(serverConnFut); + + URI wsUri = server.getWsUri(); + ClientUpgradeRequest upgradeRequest = new ClientUpgradeRequest(); + // actual value for this test is irrelevant, its important that this + // header actually be sent with a value (the value specified) + upgradeRequest.setHeader("Authorization", "Basic YWxhZGRpbjpvcGVuc2VzYW1l"); + Future future = client.connect(wsocket,wsUri,upgradeRequest); + + try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) + { + HttpFields upgradeRequestHeaders = serverConn.getUpgradeRequestHeaders(); + + Session sess = future.get(30, TimeUnit.SECONDS); + + HttpField authHeader = upgradeRequestHeaders.getField(HttpHeader.AUTHORIZATION); + assertThat("Server Request Authorization Header", authHeader, is(notNullValue())); + assertThat("Server Request Authorization Value", authHeader.getValue(), is("Basic YWxhZGRpbjpvcGVuc2VzYW1l")); + assertThat("Connect.UpgradeRequest", wsocket.connectUpgradeRequest, notNullValue()); + assertThat("Connect.UpgradeResponse", wsocket.connectUpgradeResponse, notNullValue()); + + sess.close(); + } + } + + @Test + public void testBadHandshake() throws Exception + { + JettyTrackingSocket wsocket = new JettyTrackingSocket(); + + // Force 404 response, no upgrade for this test + server.setRequestHandling((req, resp) -> { + resp.setStatus(HttpServletResponse.SC_NOT_FOUND); + return true; + }); + + URI wsUri = server.getWsUri(); + Future future = client.connect(wsocket,wsUri); + + // The attempt to get upgrade response future should throw error + ExecutionException e = assertThrows(ExecutionException.class, + ()-> future.get(30,TimeUnit.SECONDS)); + + UpgradeException ue = assertExpectedError(e,wsocket,instanceOf(UpgradeException.class)); + assertThat("UpgradeException.requestURI",ue.getRequestURI(),notNullValue()); + assertThat("UpgradeException.requestURI",ue.getRequestURI().toASCIIString(),is(wsUri.toASCIIString())); + assertThat("UpgradeException.responseStatusCode",ue.getResponseStatusCode(),is(404)); + } + + @Test + public void testBadHandshake_GetOK() throws Exception + { + JettyTrackingSocket wsocket = new JettyTrackingSocket(); + + // Force 200 response, no response body content, no upgrade for this test + server.setRequestHandling((req, resp) -> { + resp.setStatus(HttpServletResponse.SC_OK); + return true; + }); + + URI wsUri = server.getWsUri(); + Future future = client.connect(wsocket,wsUri); + + // The attempt to get upgrade response future should throw error + ExecutionException e = assertThrows(ExecutionException.class, + ()-> future.get(30,TimeUnit.SECONDS)); + + UpgradeException ue = assertExpectedError(e,wsocket,instanceOf(UpgradeException.class)); + assertThat("UpgradeException.requestURI",ue.getRequestURI(),notNullValue()); + assertThat("UpgradeException.requestURI",ue.getRequestURI().toASCIIString(),is(wsUri.toASCIIString())); + assertThat("UpgradeException.responseStatusCode",ue.getResponseStatusCode(),is(200)); + } + + @Test + public void testBadHandshake_GetOK_WithSecWebSocketAccept() throws Exception + { + JettyTrackingSocket wsocket = new JettyTrackingSocket(); + + // Force 200 response, no response body content, incomplete websocket response headers, no actual upgrade for this test + server.setRequestHandling((req, resp) -> { + String key = req.getHeader(HttpHeader.SEC_WEBSOCKET_KEY.toString()); + resp.setStatus(HttpServletResponse.SC_OK); + resp.setHeader(HttpHeader.SEC_WEBSOCKET_ACCEPT.toString(), AcceptHash.hashKey(key)); + return true; + }); + + URI wsUri = server.getWsUri(); + Future future = client.connect(wsocket,wsUri); + + // The attempt to get upgrade response future should throw error + ExecutionException e = assertThrows(ExecutionException.class, + ()-> future.get(30,TimeUnit.SECONDS)); + + UpgradeException ue = assertExpectedError(e,wsocket,instanceOf(UpgradeException.class)); + assertThat("UpgradeException.requestURI",ue.getRequestURI(),notNullValue()); + assertThat("UpgradeException.requestURI",ue.getRequestURI().toASCIIString(),is(wsUri.toASCIIString())); + assertThat("UpgradeException.responseStatusCode",ue.getResponseStatusCode(),is(200)); + } + + @Test + public void testBadHandshake_SwitchingProtocols_InvalidConnectionHeader() throws Exception + { + JettyTrackingSocket wsocket = new JettyTrackingSocket(); + + // Force 101 response, with invalid Connection header, invalid handshake + server.setRequestHandling((req, resp) -> { + String key = req.getHeader(HttpHeader.SEC_WEBSOCKET_KEY.toString()); + resp.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS); + resp.setHeader(HttpHeader.CONNECTION.toString(), "close"); + resp.setHeader(HttpHeader.SEC_WEBSOCKET_ACCEPT.toString(), AcceptHash.hashKey(key)); + return true; + }); + + URI wsUri = server.getWsUri(); + Future future = client.connect(wsocket,wsUri); + + // The attempt to get upgrade response future should throw error + ExecutionException e = assertThrows(ExecutionException.class, + ()-> future.get(30,TimeUnit.SECONDS)); + + UpgradeException ue = assertExpectedError(e,wsocket,instanceOf(UpgradeException.class)); + assertThat("UpgradeException.requestURI",ue.getRequestURI(),notNullValue()); + assertThat("UpgradeException.requestURI",ue.getRequestURI().toASCIIString(),is(wsUri.toASCIIString())); + assertThat("UpgradeException.responseStatusCode",ue.getResponseStatusCode(),is(101)); + } + + @Test + public void testBadHandshake_SwitchingProtocols_NoConnectionHeader() throws Exception + { + JettyTrackingSocket wsocket = new JettyTrackingSocket(); + + // Force 101 response, with no Connection header, invalid handshake + server.setRequestHandling((req, resp) -> { + String key = req.getHeader(HttpHeader.SEC_WEBSOCKET_KEY.toString()); + resp.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS); + // Intentionally leave out Connection header + resp.setHeader(HttpHeader.SEC_WEBSOCKET_ACCEPT.toString(), AcceptHash.hashKey(key)); + return true; + }); + + URI wsUri = server.getWsUri(); + Future future = client.connect(wsocket,wsUri); + + // The attempt to get upgrade response future should throw error + ExecutionException e = assertThrows(ExecutionException.class, + ()-> future.get(30,TimeUnit.SECONDS)); + + UpgradeException ue = assertExpectedError(e,wsocket,instanceOf(UpgradeException.class)); + assertThat("UpgradeException.requestURI",ue.getRequestURI(),notNullValue()); + assertThat("UpgradeException.requestURI",ue.getRequestURI().toASCIIString(),is(wsUri.toASCIIString())); + assertThat("UpgradeException.responseStatusCode",ue.getResponseStatusCode(),is(101)); + } + + @Test + public void testBadUpgrade() throws Exception + { + JettyTrackingSocket wsocket = new JettyTrackingSocket(); + + // Force 101 response, with invalid response accept header + server.setRequestHandling((req, resp) -> { + resp.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS); + resp.setHeader(HttpHeader.SEC_WEBSOCKET_ACCEPT.toString(), "rubbish"); + return true; + }); + + URI wsUri = server.getWsUri(); + Future future = client.connect(wsocket,wsUri); + + // The attempt to get upgrade response future should throw error + ExecutionException e = assertThrows(ExecutionException.class, + ()-> future.get(30,TimeUnit.SECONDS)); + + UpgradeException ue = assertExpectedError(e,wsocket,instanceOf(UpgradeException.class)); + assertThat("UpgradeException.requestURI",ue.getRequestURI(),notNullValue()); + assertThat("UpgradeException.requestURI",ue.getRequestURI().toASCIIString(),is(wsUri.toASCIIString())); + assertThat("UpgradeException.responseStatusCode",ue.getResponseStatusCode(),is(101)); + } + + @Test + public void testConnectionNotAccepted() throws Exception + { + JettyTrackingSocket wsocket = new JettyTrackingSocket(); + + try(ServerSocket serverSocket = new ServerSocket()) + { + InetAddress addr = InetAddress.getByName("localhost"); + InetSocketAddress endpoint = new InetSocketAddress(addr, 0); + serverSocket.bind(endpoint, 1); + int port = serverSocket.getLocalPort(); + URI wsUri = URI.create(String.format("ws://%s:%d/", addr.getHostAddress(), port)); + Future future = client.connect(wsocket, wsUri); + + // Intentionally not accept incoming socket. + // serverSocket.accept(); + + try + { + future.get(3, TimeUnit.SECONDS); + fail("Should have Timed Out"); + } + catch (ExecutionException e) + { + assertExpectedError(e, wsocket, instanceOf(UpgradeException.class)); + // Possible Passing Path (active session wait timeout) + wsocket.assertNotOpened(); + } + catch (TimeoutException e) + { + // Possible Passing Path (concurrency timeout) + wsocket.assertNotOpened(); + } + } + } + + @Test + public void testConnectionRefused() throws Exception + { + JettyTrackingSocket wsocket = new JettyTrackingSocket(); + + // Intentionally bad port with nothing listening on it + URI wsUri = new URI("ws://127.0.0.1:1"); + + try + { + Future future = client.connect(wsocket,wsUri); + + // The attempt to get upgrade response future should throw error + future.get(3,TimeUnit.SECONDS); + fail("Expected ExecutionException -> ConnectException"); + } + catch (ConnectException e) + { + Throwable t = wsocket.errorQueue.remove(); + assertThat("Error Queue[0]",t,instanceOf(ConnectException.class)); + wsocket.assertNotOpened(); + } + catch (ExecutionException e) + { + assertExpectedError(e, wsocket, + anyOf( + instanceOf(UpgradeException.class), + instanceOf(SocketTimeoutException.class), + instanceOf(ConnectException.class))); + } + } + + @Test + public void testConnectionTimeout_Concurrent() throws Exception + { + JettyTrackingSocket wsocket = new JettyTrackingSocket(); + + try(ServerSocket serverSocket = new ServerSocket()) + { + InetAddress addr = InetAddress.getByName("localhost"); + InetSocketAddress endpoint = new InetSocketAddress(addr, 0); + serverSocket.bind(endpoint, 1); + int port = serverSocket.getLocalPort(); + URI wsUri = URI.create(String.format("ws://%s:%d/", addr.getHostAddress(), port)); + Future future = client.connect(wsocket, wsUri); + + // Accept the connection, but do nothing on it (no response, no upgrade, etc) + serverSocket.accept(); + + // The attempt to get upgrade response future should throw error + Exception e = assertThrows(Exception.class, + ()-> future.get(3, TimeUnit.SECONDS)); + + if (e instanceof ExecutionException) + { + assertExpectedError((ExecutionException) e, wsocket, anyOf( + instanceOf(ConnectException.class), + instanceOf(UpgradeException.class) + )); + } + else + { + assertThat("Should have been a TimeoutException", e, instanceOf(TimeoutException.class)); + } + } + } +} diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ConnectionManagerTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ConnectionManagerTest.java new file mode 100644 index 00000000000..0c5a4ae73cb --- /dev/null +++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ConnectionManagerTest.java @@ -0,0 +1,78 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 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.client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; + +import org.eclipse.jetty.websocket.client.io.ConnectionManager; + +import org.junit.jupiter.api.Test; + +public class ConnectionManagerTest +{ + private void assertToSocketAddress(String uriStr, String expectedHost, int expectedPort) throws URISyntaxException + { + URI uri = new URI(uriStr); + + InetSocketAddress addr = ConnectionManager.toSocketAddress(uri); + assertThat("URI (" + uri + ").host",addr.getHostName(),is(expectedHost)); + assertThat("URI (" + uri + ").port",addr.getPort(),is(expectedPort)); + } + + @Test + public void testToSocketAddress_AltWsPort() throws Exception + { + assertToSocketAddress("ws://localhost:8099","localhost",8099); + } + + @Test + public void testToSocketAddress_AltWssPort() throws Exception + { + assertToSocketAddress("wss://localhost","localhost",443); + } + + @Test + public void testToSocketAddress_DefaultWsPort() throws Exception + { + assertToSocketAddress("ws://localhost","localhost",80); + } + + @Test + public void testToSocketAddress_DefaultWsPort_Path() throws Exception + { + assertToSocketAddress("ws://localhost/sockets/chat","localhost",80); + } + + @Test + public void testToSocketAddress_DefaultWssPort() throws Exception + { + assertToSocketAddress("wss://localhost:9443","localhost",9443); + } + + @Test + public void testToSocketAddress_DefaultWssPort_Path() throws Exception + { + assertToSocketAddress("wss://localhost/sockets/chat","localhost",443); + } +} diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/CookieTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/CookieTest.java new file mode 100644 index 00000000000..d869d88ce8e --- /dev/null +++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/CookieTest.java @@ -0,0 +1,212 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 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.client; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.CookieManager; +import java.net.HttpCookie; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.StatusCode; +import org.eclipse.jetty.websocket.api.WebSocketAdapter; +import org.eclipse.jetty.websocket.common.CloseInfo; +import org.eclipse.jetty.websocket.common.frames.TextFrame; +import org.eclipse.jetty.websocket.common.test.BlockheadConnection; +import org.eclipse.jetty.websocket.common.test.BlockheadServer; +import org.eclipse.jetty.websocket.common.test.Timeouts; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.AfterAll; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class CookieTest +{ + private static final Logger LOG = Log.getLogger(CookieTest.class); + + public static class CookieTrackingSocket extends WebSocketAdapter + { + public LinkedBlockingQueue messageQueue = new LinkedBlockingQueue<>(); + public LinkedBlockingQueue errorQueue = new LinkedBlockingQueue<>(); + private CountDownLatch openLatch = new CountDownLatch(1); + + @Override + public void onWebSocketConnect(Session sess) + { + openLatch.countDown(); + super.onWebSocketConnect(sess); + } + + @Override + public void onWebSocketText(String message) + { + System.err.printf("onTEXT - %s%n",message); + messageQueue.add(message); + } + + @Override + public void onWebSocketError(Throwable cause) + { + System.err.printf("onERROR - %s%n",cause); + errorQueue.add(cause); + } + + public void awaitOpen(int duration, TimeUnit unit) throws InterruptedException + { + assertTrue(openLatch.await(duration,unit), "Open Latch"); + } + } + + private static BlockheadServer server; + private WebSocketClient client; + + @BeforeEach + public void startClient() throws Exception + { + client = new WebSocketClient(); + client.start(); + } + + @BeforeAll + public static void startServer() throws Exception + { + server = new BlockheadServer(); + server.start(); + } + + @AfterEach + public void stopClient() throws Exception + { + if (client.isRunning()) + { + client.stop(); + } + } + + @AfterAll + public static void stopServer() throws Exception + { + server.stop(); + } + + @Test + public void testViaCookieManager() throws Exception + { + // Setup client + CookieManager cookieMgr = new CookieManager(); + client.setCookieStore(cookieMgr.getCookieStore()); + HttpCookie cookie = new HttpCookie("hello","world"); + cookie.setPath("/"); + cookie.setVersion(0); + cookie.setMaxAge(100000); + cookieMgr.getCookieStore().add(server.getWsUri(),cookie); + + cookie = new HttpCookie("foo","bar is the word"); + cookie.setPath("/"); + cookie.setMaxAge(100000); + cookieMgr.getCookieStore().add(server.getWsUri(),cookie); + + // Hook into server connection creation + CompletableFuture serverConnFut = new CompletableFuture<>(); + server.addConnectFuture(serverConnFut); + + // Client connects + CookieTrackingSocket clientSocket = new CookieTrackingSocket(); + Future clientConnectFuture = client.connect(clientSocket,server.getWsUri()); + + try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) + { + // client confirms upgrade and receipt of frame + String serverCookies = confirmClientUpgradeAndCookies(clientSocket, clientConnectFuture, serverConn); + + assertThat("Cookies seen at server side", serverCookies, containsString("hello=world")); + assertThat("Cookies seen at server side", serverCookies, containsString("foo=bar is the word")); + } + } + + @Test + public void testViaServletUpgradeRequest() throws Exception + { + // Setup client + HttpCookie cookie = new HttpCookie("hello","world"); + cookie.setPath("/"); + cookie.setMaxAge(100000); + + ClientUpgradeRequest request = new ClientUpgradeRequest(); + request.setCookies(Collections.singletonList(cookie)); + + // Hook into server connection creation + CompletableFuture serverConnFut = new CompletableFuture<>(); + server.addConnectFuture(serverConnFut); + + // Client connects + CookieTrackingSocket clientSocket = new CookieTrackingSocket(); + Future clientConnectFuture = client.connect(clientSocket,server.getWsUri(),request); + + try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) + { + // client confirms upgrade and receipt of frame + String serverCookies = confirmClientUpgradeAndCookies(clientSocket, clientConnectFuture, serverConn); + + assertThat("Cookies seen at server side", serverCookies, containsString("hello=world")); + } + } + + private String confirmClientUpgradeAndCookies(CookieTrackingSocket clientSocket, Future clientConnectFuture, BlockheadConnection serverConn) + throws Exception + { + // Server side upgrade information + HttpFields upgradeRequestHeaders = serverConn.getUpgradeRequestHeaders(); + HttpField cookieField = upgradeRequestHeaders.getField(HttpHeader.COOKIE); + + // Server responds with cookies it knows about + TextFrame serverCookieFrame = new TextFrame(); + serverCookieFrame.setFin(true); + serverCookieFrame.setPayload(cookieField.getValue()); + serverConn.write(serverCookieFrame); + + // Confirm client connect on future + clientConnectFuture.get(10,TimeUnit.SECONDS); + clientSocket.awaitOpen(2,TimeUnit.SECONDS); + + // Wait for client receipt of cookie frame via client websocket + String cookies = clientSocket.messageQueue.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT); + LOG.debug("Cookies seen at server: {}",cookies); + + // Server closes connection + serverConn.write(new CloseInfo(StatusCode.NORMAL).asFrame()); + + return cookies; + } +} diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/JettyTrackingSocket.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/JettyTrackingSocket.java new file mode 100644 index 00000000000..246010401bb --- /dev/null +++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/JettyTrackingSocket.java @@ -0,0 +1,178 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 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.client; + +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Exchanger; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.UpgradeRequest; +import org.eclipse.jetty.websocket.api.UpgradeResponse; +import org.eclipse.jetty.websocket.api.WebSocketAdapter; +import org.eclipse.jetty.websocket.common.test.Timeouts; + + +/** + * Testing Socket used on client side WebSocket testing. + */ +public class JettyTrackingSocket extends WebSocketAdapter +{ + private static final Logger LOG = Log.getLogger(JettyTrackingSocket.class); + + public int closeCode = -1; + public Exchanger messageExchanger; + public UpgradeRequest connectUpgradeRequest; + public UpgradeResponse connectUpgradeResponse; + public StringBuilder closeMessage = new StringBuilder(); + public CountDownLatch openLatch = new CountDownLatch(1); + public CountDownLatch closeLatch = new CountDownLatch(1); + public CountDownLatch dataLatch = new CountDownLatch(1); + public LinkedBlockingQueue messageQueue = new LinkedBlockingQueue<>(); + public LinkedBlockingQueue errorQueue = new LinkedBlockingQueue<>(); + + public void assertClose(int expectedStatusCode, String expectedReason) throws InterruptedException + { + assertCloseCode(expectedStatusCode); + assertCloseReason(expectedReason); + } + + public void assertCloseCode(int expectedCode) throws InterruptedException + { + assertThat("Was Closed",closeLatch.await(50,TimeUnit.MILLISECONDS),is(true)); + assertThat("Close Code / Received [" + closeMessage + "]",closeCode,is(expectedCode)); + } + + private void assertCloseReason(String expectedReason) + { + assertThat("Close Reason",closeMessage.toString(),is(expectedReason)); + } + + public void assertIsOpen() throws InterruptedException + { + assertWasOpened(); + assertNotClosed(); + } + + public void assertNotClosed() + { + LOG.debug("assertNotClosed() - {}", closeLatch.getCount()); + assertThat("Closed Latch",closeLatch.getCount(),greaterThanOrEqualTo(1L)); + } + + public void assertNotOpened() + { + LOG.debug("assertNotOpened() - {}", openLatch.getCount()); + assertThat("Open Latch",openLatch.getCount(),greaterThanOrEqualTo(1L)); + } + + public void assertWasOpened() throws InterruptedException + { + LOG.debug("assertWasOpened() - {}", openLatch.getCount()); + assertThat("Was Opened",openLatch.await(30,TimeUnit.SECONDS),is(true)); + } + + public void clear() + { + messageQueue.clear(); + } + + @Override + public void onWebSocketBinary(byte[] payload, int offset, int len) + { + LOG.debug("onWebSocketBinary()"); + dataLatch.countDown(); + } + + @Override + public void onWebSocketClose(int statusCode, String reason) + { + LOG.debug("onWebSocketClose({},{})",statusCode,reason); + super.onWebSocketClose(statusCode,reason); + closeCode = statusCode; + closeMessage.append(reason); + closeLatch.countDown(); + } + + @Override + public void onWebSocketConnect(Session session) + { + super.onWebSocketConnect(session); + assertThat("Session", session, notNullValue()); + connectUpgradeRequest = session.getUpgradeRequest(); + connectUpgradeResponse = session.getUpgradeResponse(); + openLatch.countDown(); + } + + @Override + public void onWebSocketError(Throwable cause) + { + LOG.debug("onWebSocketError",cause); + assertThat("Error capture",errorQueue.offer(cause),is(true)); + } + + @Override + public void onWebSocketText(String message) + { + LOG.debug("onWebSocketText({})",message); + messageQueue.offer(message); + dataLatch.countDown(); + + if (messageExchanger != null) + { + try + { + messageExchanger.exchange(message); + } + catch (InterruptedException e) + { + LOG.debug(e); + } + } + } + + public void waitForClose(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException + { + assertThat("Client Socket Closed",closeLatch.await(timeoutDuration,timeoutUnit),is(true)); + } + + public void waitForConnected() throws InterruptedException + { + assertThat("Client Socket Connected",openLatch.await(Timeouts.CONNECT,Timeouts.CONNECT_UNIT),is(true)); + } + + public void waitForMessage(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException + { + LOG.debug("Waiting for message"); + assertThat("Message Received",dataLatch.await(timeoutDuration,timeoutUnit),is(true)); + } + + public void close() + { + getSession().close(); + } +} diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SessionTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SessionTest.java new file mode 100644 index 00000000000..ca553545db3 --- /dev/null +++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SessionTest.java @@ -0,0 +1,135 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 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.client; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.net.URI; +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.websocket.api.BatchMode; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.common.WebSocketFrame; +import org.eclipse.jetty.websocket.common.WebSocketSession; +import org.eclipse.jetty.websocket.common.test.BlockheadConnection; +import org.eclipse.jetty.websocket.common.test.BlockheadServer; +import org.eclipse.jetty.websocket.common.test.Timeouts; +import org.junit.jupiter.api.AfterAll; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class SessionTest +{ + private static BlockheadServer server; + + @BeforeAll + public static void startServer() throws Exception + { + server = new BlockheadServer(); + server.start(); + } + + @AfterAll + public static void stopServer() throws Exception + { + server.stop(); + } + + @Test + @Disabled // TODO fix frequent failure + public void testBasicEcho_FromClient() throws Exception + { + WebSocketClient client = new WebSocketClient(); + client.start(); + try + { + JettyTrackingSocket cliSock = new JettyTrackingSocket(); + + // Hook into server connection creation + CompletableFuture serverConnFut = new CompletableFuture<>(); + server.addConnectFuture(serverConnFut); + + client.getPolicy().setIdleTimeout(10000); + + URI wsUri = server.getWsUri(); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + request.setSubProtocols("echo"); + Future future = client.connect(cliSock,wsUri,request); + + try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) + { + // Setup echo of frames on server side + serverConn.setIncomingFrameConsumer((frame)->{ + WebSocketFrame copy = WebSocketFrame.copy(frame); + serverConn.write(copy); + }); + + Session sess = future.get(30000, TimeUnit.MILLISECONDS); + assertThat("Session", sess, notNullValue()); + assertThat("Session.open", sess.isOpen(), is(true)); + assertThat("Session.upgradeRequest", sess.getUpgradeRequest(), notNullValue()); + assertThat("Session.upgradeResponse", sess.getUpgradeResponse(), notNullValue()); + + cliSock.assertWasOpened(); + cliSock.assertNotClosed(); + + Collection sessions = client.getBeans(WebSocketSession.class); + assertThat("client.connectionManager.sessions.size", sessions.size(), is(1)); + + RemoteEndpoint remote = cliSock.getSession().getRemote(); + remote.sendStringByFuture("Hello World!"); + if (remote.getBatchMode() == BatchMode.ON) + { + remote.flush(); + } + + // wait for response from server + cliSock.waitForMessage(30000, TimeUnit.MILLISECONDS); + + Set open = client.getOpenSessions(); + assertThat("(Before Close) Open Sessions.size", open.size(), is(1)); + + String received = cliSock.messageQueue.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT); + assertThat("Message", received, containsString("Hello World!")); + + cliSock.close(); + } + + cliSock.waitForClose(30000, TimeUnit.MILLISECONDS); + Set open = client.getOpenSessions(); + + // TODO this sometimes fails! + assertThat("(After Close) Open Sessions.size", open.size(), is(0)); + } + finally + { + client.stop(); + } + } +} diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowClientTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowClientTest.java new file mode 100644 index 00000000000..1cac5b5e889 --- /dev/null +++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowClientTest.java @@ -0,0 +1,133 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 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.client; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.StatusCode; +import org.eclipse.jetty.websocket.common.CloseInfo; +import org.eclipse.jetty.websocket.common.OpCode; +import org.eclipse.jetty.websocket.common.WebSocketFrame; +import org.eclipse.jetty.websocket.common.test.BlockheadConnection; +import org.eclipse.jetty.websocket.common.test.BlockheadServer; +import org.eclipse.jetty.websocket.common.test.Timeouts; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.AfterAll; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class SlowClientTest +{ + private static BlockheadServer server; + private WebSocketClient client; + + @BeforeEach + public void startClient() throws Exception + { + client = new WebSocketClient(); + client.getPolicy().setIdleTimeout(60000); + client.start(); + } + + @BeforeAll + public static void startServer() throws Exception + { + server = new BlockheadServer(); + server.start(); + } + + @AfterEach + public void stopClient() throws Exception + { + client.stop(); + } + + @AfterAll + public static void stopServer() throws Exception + { + server.stop(); + } + + @Test + public void testClientSlowToSend() throws Exception + { + JettyTrackingSocket tsocket = new JettyTrackingSocket(); + client.getPolicy().setIdleTimeout(60000); + + URI wsUri = server.getWsUri(); + Future future = client.connect(tsocket, wsUri); + + CompletableFuture serverConnFut = new CompletableFuture<>(); + server.addConnectFuture(serverConnFut); + + // Confirm connected + future.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT); + tsocket.waitForConnected(); + + int messageCount = 10; + + try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) + { + // Have client write slowly. + ClientWriteThread writer = new ClientWriteThread(tsocket.getSession()); + writer.setMessageCount(messageCount); + writer.setMessage("Hello"); + writer.setSlowness(10); + writer.start(); + writer.join(); + + LinkedBlockingQueue serverFrames = serverConn.getFrameQueue(); + + for (int i = 0; i < messageCount; i++) + { + WebSocketFrame serverFrame = serverFrames.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT); + String prefix = "Server frame[" + i + "]"; + assertThat(prefix + ".opcode", serverFrame.getOpCode(), is(OpCode.TEXT)); + assertThat(prefix + ".payload", serverFrame.getPayloadAsUTF8(), is("Hello/" + i + "/")); + } + + // Close + tsocket.getSession().close(StatusCode.NORMAL, "Done"); + + // confirm close received on server + WebSocketFrame serverFrame = serverFrames.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT); + assertThat("close frame", serverFrame.getOpCode(), is(OpCode.CLOSE)); + CloseInfo closeInfo = new CloseInfo(serverFrame); + assertThat("close info", closeInfo.getStatusCode(), is(StatusCode.NORMAL)); + WebSocketFrame respClose = WebSocketFrame.copy(serverFrame); + respClose.setMask(null); // remove client mask (if present) + serverConn.write(respClose); + + // Verify server response + assertTrue(tsocket.closeLatch.await(3, TimeUnit.MINUTES), "Client Socket Closed"); + tsocket.assertCloseCode(StatusCode.NORMAL); + } + } +} diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowServerTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowServerTest.java new file mode 100644 index 00000000000..08ce4d1b029 --- /dev/null +++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowServerTest.java @@ -0,0 +1,160 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 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.client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.client.masks.ZeroMasker; +import org.eclipse.jetty.websocket.common.OpCode; +import org.eclipse.jetty.websocket.common.WebSocketFrame; +import org.eclipse.jetty.websocket.common.test.BlockheadConnection; +import org.eclipse.jetty.websocket.common.test.BlockheadServer; +import org.eclipse.jetty.websocket.common.test.Timeouts; +import org.junit.jupiter.api.AfterEach; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class SlowServerTest +{ + private BlockheadServer server; + private WebSocketClient client; + + @BeforeEach + public void startClient() throws Exception + { + client = new WebSocketClient(); + client.setMaxIdleTimeout(60000); + client.start(); + } + + @BeforeEach + public void startServer() throws Exception + { + server = new BlockheadServer(); + server.start(); + } + + @AfterEach + public void stopClient() throws Exception + { + client.stop(); + } + + @AfterEach + public void stopServer() throws Exception + { + server.stop(); + } + + @Test + public void testServerSlowToRead() throws Exception + { + JettyTrackingSocket tsocket = new JettyTrackingSocket(); + client.setMasker(new ZeroMasker()); + client.setMaxIdleTimeout(60000); + + CompletableFuture serverConnFut = new CompletableFuture<>(); + server.addConnectFuture(serverConnFut); + + URI wsUri = server.getWsUri(); + Future future = client.connect(tsocket,wsUri); + + try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) + { + // slow down reads + serverConn.setIncomingFrameConsumer((frame)-> { + try + { + TimeUnit.MILLISECONDS.sleep(100); + } + catch (InterruptedException ignore) + { + } + }); + + // Confirm connected + future.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT); + tsocket.waitForConnected(); + + int messageCount = 10; + + // Have client write as quickly as it can. + ClientWriteThread writer = new ClientWriteThread(tsocket.getSession()); + writer.setMessageCount(messageCount); + writer.setMessage("Hello"); + writer.setSlowness(-1); // disable slowness + writer.start(); + writer.join(); + + // Verify receive + LinkedBlockingQueue serverFrames = serverConn.getFrameQueue(); + for(int i=0; i< messageCount; i++) + { + WebSocketFrame serverFrame = serverFrames.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT); + String prefix = "Server Frame[" + i + "]"; + assertThat(prefix, serverFrame, is(notNullValue())); + assertThat(prefix + ".opCode", serverFrame.getOpCode(), is(OpCode.TEXT)); + assertThat(prefix + ".payload", serverFrame.getPayloadAsUTF8(), is("Hello/" + i + "/")); + } + } + } + + @Test + public void testServerSlowToSend() throws Exception + { + JettyTrackingSocket clientSocket = new JettyTrackingSocket(); + client.setMaxIdleTimeout(60000); + + CompletableFuture serverConnFut = new CompletableFuture<>(); + server.addConnectFuture(serverConnFut); + + URI wsUri = server.getWsUri(); + Future clientConnectFuture = client.connect(clientSocket,wsUri); + + try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) + { + // Confirm connected + clientConnectFuture.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT); + clientSocket.waitForConnected(); + + // Have server write slowly. + int messageCount = 1000; + + ServerWriteThread writer = new ServerWriteThread(serverConn); + writer.setMessageCount(messageCount); + writer.setMessage("Hello"); + writer.setSlowness(10); + writer.start(); + writer.join(); + + // Verify receive + assertThat("Message Receive Count", clientSocket.messageQueue.size(), is(messageCount)); + } + } +} diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/WebSocketClientTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/WebSocketClientTest.java new file mode 100644 index 00000000000..4208baf7d1a --- /dev/null +++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/WebSocketClientTest.java @@ -0,0 +1,328 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 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.client; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.websocket.api.BatchMode; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.UpgradeRequest; +import org.eclipse.jetty.websocket.common.WebSocketFrame; +import org.eclipse.jetty.websocket.common.WebSocketSession; +import org.eclipse.jetty.websocket.common.frames.TextFrame; +import org.eclipse.jetty.websocket.common.io.FutureWriteCallback; +import org.eclipse.jetty.websocket.common.test.BlockheadConnection; +import org.eclipse.jetty.websocket.common.test.BlockheadServer; +import org.eclipse.jetty.websocket.common.test.Timeouts; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class WebSocketClientTest +{ + private static BlockheadServer server; + private WebSocketClient client; + + @BeforeEach + public void startClient() throws Exception + { + client = new WebSocketClient(); + client.start(); + } + + @BeforeAll + public static void startServer() throws Exception + { + server = new BlockheadServer(); + server.getPolicy().setMaxTextMessageSize(200 * 1024); + server.getPolicy().setMaxBinaryMessageSize(200 * 1024); + server.start(); + } + + @AfterEach + public void stopClient() throws Exception + { + client.stop(); + } + + @AfterAll + public static void stopServer() throws Exception + { + server.stop(); + } + + @Test + public void testAddExtension_NotInstalled() throws Exception + { + JettyTrackingSocket cliSock = new JettyTrackingSocket(); + + client.getPolicy().setIdleTimeout(10000); + + URI wsUri = server.getWsUri(); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + request.setSubProtocols("echo"); + request.addExtensions("x-bad"); + + assertThrows(IllegalArgumentException.class, ()-> { + // Should trigger failure on bad extension + client.connect(cliSock, wsUri, request); + }); + } + + @Test + public void testBasicEcho_FromClient() throws Exception + { + JettyTrackingSocket cliSock = new JettyTrackingSocket(); + + client.getPolicy().setIdleTimeout(10000); + + // Hook into server connection creation + CompletableFuture serverConnFut = new CompletableFuture<>(); + server.addConnectFuture(serverConnFut); + + URI wsUri = server.getWsUri(); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + request.setSubProtocols("echo"); + Future future = client.connect(cliSock,wsUri,request); + + try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) + { + // Setup echo of frames on server side + serverConn.setIncomingFrameConsumer((frame)->{ + WebSocketFrame copy = WebSocketFrame.copy(frame); + copy.setMask(null); // strip client mask (if present) + serverConn.write(copy); + }); + + Session sess = future.get(30,TimeUnit.SECONDS); + assertThat("Session",sess,notNullValue()); + assertThat("Session.open",sess.isOpen(),is(true)); + assertThat("Session.upgradeRequest",sess.getUpgradeRequest(),notNullValue()); + assertThat("Session.upgradeResponse",sess.getUpgradeResponse(),notNullValue()); + + cliSock.assertWasOpened(); + cliSock.assertNotClosed(); + + Collection sessions = client.getOpenSessions(); + assertThat("client.connectionManager.sessions.size",sessions.size(),is(1)); + + RemoteEndpoint remote = cliSock.getSession().getRemote(); + remote.sendStringByFuture("Hello World!"); + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); + + // wait for response from server + String received = cliSock.messageQueue.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT); + assertThat("Message", received, containsString("Hello World")); + } + } + + @Test + public void testBasicEcho_UsingCallback() throws Exception + { + client.setMaxIdleTimeout(160000); + JettyTrackingSocket cliSock = new JettyTrackingSocket(); + + // Hook into server connection creation + CompletableFuture serverConnFut = new CompletableFuture<>(); + server.addConnectFuture(serverConnFut); + + URI wsUri = server.getWsUri(); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + request.setSubProtocols("echo"); + Future future = client.connect(cliSock,wsUri,request); + + try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) + { + Session sess = future.get(30, TimeUnit.SECONDS); + assertThat("Session", sess, notNullValue()); + assertThat("Session.open", sess.isOpen(), is(true)); + assertThat("Session.upgradeRequest", sess.getUpgradeRequest(), notNullValue()); + assertThat("Session.upgradeResponse", sess.getUpgradeResponse(), notNullValue()); + + cliSock.assertWasOpened(); + cliSock.assertNotClosed(); + + Collection sessions = client.getBeans(WebSocketSession.class); + assertThat("client.connectionManager.sessions.size", sessions.size(), is(1)); + + FutureWriteCallback callback = new FutureWriteCallback(); + + cliSock.getSession().getRemote().sendString("Hello World!", callback); + callback.get(1, TimeUnit.SECONDS); + } + } + + @Test + public void testBasicEcho_FromServer() throws Exception + { + // Hook into server connection creation + CompletableFuture serverConnFut = new CompletableFuture<>(); + server.addConnectFuture(serverConnFut); + + JettyTrackingSocket wsocket = new JettyTrackingSocket(); + Future future = client.connect(wsocket,server.getWsUri()); + + try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) + { + // Validate connect + Session sess = future.get(30, TimeUnit.SECONDS); + assertThat("Session", sess, notNullValue()); + assertThat("Session.open", sess.isOpen(), is(true)); + assertThat("Session.upgradeRequest", sess.getUpgradeRequest(), notNullValue()); + assertThat("Session.upgradeResponse", sess.getUpgradeResponse(), notNullValue()); + + // Have server send initial message + serverConn.write(new TextFrame().setPayload("Hello World")); + + // Verify connect + future.get(30, TimeUnit.SECONDS); + wsocket.assertWasOpened(); + + String received = wsocket.messageQueue.poll(Timeouts.POLL_EVENT, Timeouts.POLL_EVENT_UNIT); + assertThat("Message", received, containsString("Hello World")); + } + } + + @Test + public void testLocalRemoteAddress() throws Exception + { + JettyTrackingSocket wsocket = new JettyTrackingSocket(); + + URI wsUri = server.getWsUri(); + Future future = client.connect(wsocket,wsUri); + + future.get(30,TimeUnit.SECONDS); + + assertTrue(wsocket.openLatch.await(1,TimeUnit.SECONDS)); + + InetSocketAddress local = wsocket.getSession().getLocalAddress(); + InetSocketAddress remote = wsocket.getSession().getRemoteAddress(); + + assertThat("Local Socket Address",local,notNullValue()); + assertThat("Remote Socket Address",remote,notNullValue()); + + // Hard to validate (in a portable unit test) the local address that was used/bound in the low level Jetty Endpoint + assertThat("Local Socket Address / Host",local.getAddress().getHostAddress(),notNullValue()); + assertThat("Local Socket Address / Port",local.getPort(),greaterThan(0)); + + String uriHostAddress = InetAddress.getByName(wsUri.getHost()).getHostAddress(); + assertThat("Remote Socket Address / Host",remote.getAddress().getHostAddress(),is(uriHostAddress)); + assertThat("Remote Socket Address / Port",remote.getPort(),greaterThan(0)); + } + + /** + * Ensure that @WebSocket(maxTextMessageSize = 100*1024) behaves as expected. + * + * @throws Exception + * on test failure + */ + @Test + public void testMaxMessageSize() throws Exception + { + MaxMessageSocket wsocket = new MaxMessageSocket(); + + // Hook into server connection creation + CompletableFuture serverConnFut = new CompletableFuture<>(); + server.addConnectFuture(serverConnFut); + + URI wsUri = server.getWsUri(); + Future future = client.connect(wsocket,wsUri); + + try (BlockheadConnection serverConn = serverConnFut.get(Timeouts.CONNECT, Timeouts.CONNECT_UNIT)) + { + // Setup echo of frames on server side + serverConn.setIncomingFrameConsumer((frame)->{ + WebSocketFrame copy = WebSocketFrame.copy(frame); + copy.setMask(null); // strip client mask (if present) + serverConn.write(copy); + }); + + wsocket.awaitConnect(1,TimeUnit.SECONDS); + + Session sess = future.get(30,TimeUnit.SECONDS); + assertThat("Session",sess,notNullValue()); + assertThat("Session.open",sess.isOpen(),is(true)); + + // Create string that is larger than default size of 64k + // but smaller than maxMessageSize of 100k + byte buf[] = new byte[80 * 1024]; + Arrays.fill(buf,(byte)'x'); + String msg = StringUtil.toUTF8String(buf,0,buf.length); + + wsocket.getSession().getRemote().sendStringByFuture(msg); + + // wait for response from server + wsocket.waitForMessage(1, TimeUnit.SECONDS); + + wsocket.assertMessage(msg); + + assertTrue(wsocket.dataLatch.await(2, TimeUnit.SECONDS)); + } + } + + @Test + public void testParameterMap() throws Exception + { + JettyTrackingSocket wsocket = new JettyTrackingSocket(); + + URI wsUri = server.getWsUri().resolve("/test?snack=cashews&amount=handful&brand=off"); + Future future = client.connect(wsocket,wsUri); + + future.get(30,TimeUnit.SECONDS); + + assertTrue(wsocket.openLatch.await(1,TimeUnit.SECONDS)); + + Session session = wsocket.getSession(); + UpgradeRequest req = session.getUpgradeRequest(); + assertThat("Upgrade Request",req,notNullValue()); + + Map> parameterMap = req.getParameterMap(); + assertThat("Parameter Map",parameterMap,notNullValue()); + + assertThat("Parameter[snack]",parameterMap.get("snack"),is(Arrays.asList(new String[] { "cashews" }))); + assertThat("Parameter[amount]",parameterMap.get("amount"),is(Arrays.asList(new String[] { "handful" }))); + assertThat("Parameter[brand]",parameterMap.get("brand"),is(Arrays.asList(new String[] { "off" }))); + + assertThat("Parameter[cost]",parameterMap.get("cost"),nullValue()); + } +}