test/jdk/java/net/httpclient/ServerCloseTest.java
changeset 49765 ee6f7a61f3a5
child 51407 910f7b56592f
child 56451 9585061fdb04
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/ServerCloseTest.java	Tue Apr 17 08:54:17 2018 -0700
@@ -0,0 +1,410 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests that our client deals correctly with servers that
+ *          close the connection right after sending the last byte.
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters EncodedCharsInURI
+ * @modules java.base/sun.net.www.http
+ *          java.net.http/jdk.internal.net.http.common
+ *          java.net.http/jdk.internal.net.http.frame
+ *          java.net.http/jdk.internal.net.http.hpack
+ * @run testng/othervm ServerCloseTest
+ */
+//*        -Djdk.internal.httpclient.debug=true
+
+import jdk.testlibrary.SimpleSSLContext;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import javax.net.ServerSocketFactory;
+import javax.net.ssl.SSLContext;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublisher;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandler;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.util.List;
+import java.util.Locale;
+import java.util.StringTokenizer;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static java.lang.System.out;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+public class ServerCloseTest implements HttpServerAdapters {
+
+    SSLContext sslContext;
+    DummyServer    httpDummyServer;    // HTTP/1.1    [ 2 servers ]
+    DummyServer    httpsDummyServer;   // HTTPS/1.1
+    String httpDummy;
+    String httpsDummy;
+
+    static final int ITERATION_COUNT = 3;
+    // a shared executor helps reduce the amount of threads created by the test
+    static final Executor executor = new TestExecutor(Executors.newCachedThreadPool());
+    static final ConcurrentMap<String, Throwable> FAILURES = new ConcurrentHashMap<>();
+    static volatile boolean tasksFailed;
+    static final AtomicLong serverCount = new AtomicLong();
+    static final AtomicLong clientCount = new AtomicLong();
+    static final long start = System.nanoTime();
+    public static String now() {
+        long now = System.nanoTime() - start;
+        long secs = now / 1000_000_000;
+        long mill = (now % 1000_000_000) / 1000_000;
+        long nan = now % 1000_000;
+        return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan);
+    }
+
+    private volatile HttpClient sharedClient;
+
+    static class TestExecutor implements Executor {
+        final AtomicLong tasks = new AtomicLong();
+        Executor executor;
+        TestExecutor(Executor executor) {
+            this.executor = executor;
+        }
+
+        @Override
+        public void execute(Runnable command) {
+            long id = tasks.incrementAndGet();
+            executor.execute(() -> {
+                try {
+                    command.run();
+                } catch (Throwable t) {
+                    tasksFailed = true;
+                    System.out.printf(now() + "Task %s failed: %s%n", id, t);
+                    System.err.printf(now() + "Task %s failed: %s%n", id, t);
+                    FAILURES.putIfAbsent("Task " + id, t);
+                    throw t;
+                }
+            });
+        }
+    }
+
+    @AfterClass
+    static final void printFailedTests() {
+        out.println("\n=========================");
+        try {
+            out.printf("%n%sCreated %d servers and %d clients%n",
+                    now(), serverCount.get(), clientCount.get());
+            if (FAILURES.isEmpty()) return;
+            out.println("Failed tests: ");
+            FAILURES.entrySet().forEach((e) -> {
+                out.printf("\t%s: %s%n", e.getKey(), e.getValue());
+                e.getValue().printStackTrace(out);
+            });
+            if (tasksFailed) {
+                System.out.println("WARNING: Some tasks failed");
+            }
+        } finally {
+            out.println("\n=========================\n");
+        }
+    }
+
+    private String[] uris() {
+        return new String[] {
+                httpDummy,
+                httpsDummy,
+        };
+    }
+
+    @DataProvider(name = "servers")
+    public Object[][] noThrows() {
+        String[] uris = uris();
+        Object[][] result = new Object[uris.length * 2][];
+        //Object[][] result = new Object[uris.length][];
+        int i = 0;
+        for (boolean sameClient : List.of(false, true)) {
+            //if (!sameClient) continue;
+            for (String uri: uris()) {
+                result[i++] = new Object[] {uri, sameClient};
+            }
+        }
+        assert i == uris.length * 2;
+        // assert i == uris.length ;
+        return result;
+    }
+
+    private HttpClient makeNewClient() {
+        clientCount.incrementAndGet();
+        return HttpClient.newBuilder()
+                .executor(executor)
+                .sslContext(sslContext)
+                .build();
+    }
+
+    HttpClient newHttpClient(boolean share) {
+        if (!share) return makeNewClient();
+        HttpClient shared = sharedClient;
+        if (shared != null) return shared;
+        synchronized (this) {
+            shared = sharedClient;
+            if (shared == null) {
+                shared = sharedClient = makeNewClient();
+            }
+            return shared;
+        }
+    }
+
+    final String ENCODED = "/01%252F03/";
+
+    @Test(dataProvider = "servers")
+    public void testServerClose(String uri, boolean sameClient) {
+        HttpClient client = null;
+        out.printf("%n%s testServerClose(%s, %b)%n", now(), uri, sameClient);
+        uri = uri + ENCODED;
+        for (int i=0; i< ITERATION_COUNT; i++) {
+            if (!sameClient || client == null)
+                client = newHttpClient(sameClient);
+
+            BodyPublisher bodyPublisher = BodyPublishers.ofString(uri);
+
+            HttpRequest req = HttpRequest.newBuilder(URI.create(uri))
+                    .POST(bodyPublisher)
+                    .build();
+            BodyHandler<String> handler = BodyHandlers.ofString();
+            HttpClient c = client;
+            CompletableFuture<HttpResponse<String>> responseCF = c.sendAsync(req, handler);
+            // retry POST if needed   #### Replace with exceptionallyCompose
+            responseCF = responseCF.handle((r,t) ->
+                    t == null ? CompletableFuture.completedFuture(r)
+                            : c.sendAsync(req, handler)).thenCompose(x -> x);
+            HttpResponse<String> response = responseCF.join();
+            String body = response.body();
+            if (!uri.contains(body)) {
+                System.err.println("Test failed: " + response);
+                throw new RuntimeException(uri + " doesn't contain '" + body + "'");
+            } else {
+                System.out.println("Found expected " + body + " in " + uri);
+            }
+        }
+    }
+
+    @BeforeTest
+    public void setup() throws Exception {
+        sslContext = new SimpleSSLContext().get();
+        if (sslContext == null)
+            throw new AssertionError("Unexpected null sslContext");
+
+        InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
+
+        // DummyServer
+        httpDummyServer = DummyServer.create(sa);
+        httpsDummyServer = DummyServer.create(sa, sslContext);
+        httpDummy = "http://" + httpDummyServer.serverAuthority() + "/http1/dummy/x";
+        httpsDummy = "https://" + httpsDummyServer.serverAuthority() + "/https1/dummy/x";
+
+
+        serverCount.addAndGet(2);
+        httpDummyServer.start();
+        httpsDummyServer.start();
+    }
+
+    @AfterTest
+    public void teardown() throws Exception {
+        sharedClient = null;
+        httpDummyServer.stopServer();
+        httpsDummyServer.stopServer();
+    }
+
+    static class DummyServer extends Thread {
+        final ServerSocket ss;
+        final boolean secure;
+        ConcurrentLinkedQueue<Socket> connections = new ConcurrentLinkedQueue<>();
+        volatile boolean stopped;
+        DummyServer(ServerSocket ss, boolean secure) {
+            super("DummyServer[" + ss.getLocalPort()+"]");
+            this.secure = secure;
+            this.ss = ss;
+        }
+
+        // This is a bit shaky. It doesn't handle continuation
+        // lines, but our client shouldn't send any.
+        // Read a line from the input stream, swallowing the final
+        // \r\n sequence. Stops at the first \n, doesn't complain
+        // if it wasn't preceded by '\r'.
+        //
+        String readLine(InputStream r) throws IOException {
+            StringBuilder b = new StringBuilder();
+            int c;
+            while ((c = r.read()) != -1) {
+                if (c == '\n') break;
+                b.appendCodePoint(c);
+            }
+            if (b.codePointAt(b.length() -1) == '\r') {
+                b.delete(b.length() -1, b.length());
+            }
+            return b.toString();
+        }
+
+        @Override
+        public void run() {
+            try {
+                while(!stopped) {
+                    Socket clientConnection = ss.accept();
+                    connections.add(clientConnection);
+                    System.out.println(now() + getName() + ": Client accepted");
+                    StringBuilder headers = new StringBuilder();
+                    Socket targetConnection = null;
+                    InputStream  ccis = clientConnection.getInputStream();
+                    OutputStream ccos = clientConnection.getOutputStream();
+                    Writer w = new OutputStreamWriter(
+                            clientConnection.getOutputStream(), "UTF-8");
+                    PrintWriter pw = new PrintWriter(w);
+                    System.out.println(now() + getName() + ": Reading request line");
+                    String requestLine = readLine(ccis);
+                    System.out.println(now() + getName() + ": Request line: " + requestLine);
+
+                    StringTokenizer tokenizer = new StringTokenizer(requestLine);
+                    String method = tokenizer.nextToken();
+                    assert method.equalsIgnoreCase("POST")
+                            || method.equalsIgnoreCase("GET");
+                    String path = tokenizer.nextToken();
+                    URI uri;
+                    try {
+                        String hostport = serverAuthority();
+                        uri = new URI((secure ? "https" : "http") +"://" + hostport + path);
+                    } catch (Throwable x) {
+                        System.err.printf("Bad target address: \"%s\" in \"%s\"%n",
+                                path, requestLine);
+                        clientConnection.close();
+                        continue;
+                    }
+
+                    // Read all headers until we find the empty line that
+                    // signals the end of all headers.
+                    String line = requestLine;
+                    while (!line.equals("")) {
+                        System.out.println(now() + getName() + ": Reading header: "
+                                + (line = readLine(ccis)));
+                        headers.append(line).append("\r\n");
+                    }
+
+                    StringBuilder response = new StringBuilder();
+
+                    int index = headers.toString()
+                            .toLowerCase(Locale.US)
+                            .indexOf("content-length: ");
+                    byte[] b = uri.toString().getBytes(UTF_8);
+                    if (index >= 0) {
+                        index = index + "content-length: ".length();
+                        String cl = headers.toString().substring(index);
+                        StringTokenizer tk = new StringTokenizer(cl);
+                        int len = Integer.parseInt(tk.nextToken());
+                        assert len < b.length * 2;
+                        System.out.println(now() + getName()
+                                + ": received body: "
+                                + new String(ccis.readNBytes(len), UTF_8));
+                    }
+                    System.out.println(now()
+                            + getName() + ": sending back " + uri);
+
+                    response.append("HTTP/1.1 200 OK\r\nContent-Length: ")
+                            .append(b.length)
+                            .append("\r\n\r\n");
+
+                    // Then send the 200 OK response to the client
+                    System.out.println(now() + getName() + ": Sending "
+                            + response);
+                    pw.print(response);
+                    pw.flush();
+                    ccos.write(b);
+                    ccos.flush();
+                    ccos.close();
+                    connections.remove(clientConnection);
+                    clientConnection.close();
+                }
+            } catch (Throwable t) {
+                if (!stopped) {
+                    System.out.println(now() + getName() + ": failed: " + t);
+                    t.printStackTrace();
+                    try {
+                        stopServer();
+                    } catch (Throwable e) {
+
+                    }
+                }
+            } finally {
+                System.out.println(now() + getName() + ": exiting");
+            }
+        }
+
+        void close(Socket s) {
+            try {
+                s.close();
+            } catch(Throwable t) {
+
+            }
+        }
+        public void stopServer() throws IOException {
+            stopped = true;
+            ss.close();
+            connections.forEach(this::close);
+        }
+
+        public String serverAuthority() {
+            return InetAddress.getLoopbackAddress().getHostName() + ":"
+                    + ss.getLocalPort();
+        }
+
+        static DummyServer create(InetSocketAddress sa) throws IOException {
+            ServerSocket ss = ServerSocketFactory.getDefault()
+                    .createServerSocket(sa.getPort(), -1, sa.getAddress());
+            return  new DummyServer(ss, false);
+        }
+
+        static DummyServer create(InetSocketAddress sa, SSLContext sslContext) throws IOException {
+            ServerSocket ss = sslContext.getServerSocketFactory()
+                    .createServerSocket(sa.getPort(), -1, sa.getAddress());
+            return new DummyServer(ss, true);
+        }
+
+
+    }
+
+}