changeset 60740:f50a7df94744

8242044: Add basic HTTP/1.1 support to the HTTP/2 Test Server Reviewed-by: dfuchs, michaelm
author chegar
date Fri, 03 Apr 2020 07:27:53 +0100
parents 398ff7d301a4
children 2502db745df8
files test/jdk/java/net/httpclient/HttpVersionsTest.java test/jdk/java/net/httpclient/http2/server/Http2TestServer.java test/jdk/java/net/httpclient/http2/server/Http2TestServerConnection.java
diffstat 3 files changed, 345 insertions(+), 29 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/HttpVersionsTest.java	Fri Apr 03 07:27:53 2020 +0100
@@ -0,0 +1,244 @@
+/*
+ * Copyright (c) 2020, 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 Checks HTTP versions when interacting with an HTTP/2 server
+ * @bug 8242044
+ * @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
+ *          java.logging
+ * @library /test/lib http2/server
+ * @build Http2TestServer
+ * @build jdk.test.lib.net.SimpleSSLContext
+ * @build jdk.test.lib.Platform
+ * @run testng/othervm HttpVersionsTest
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import javax.net.ssl.SSLContext;
+import jdk.test.lib.net.SimpleSSLContext;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import static java.lang.String.format;
+import static java.lang.System.out;
+import static java.net.http.HttpClient.Version.HTTP_1_1;
+import static java.net.http.HttpClient.Version.HTTP_2;
+import static java.net.http.HttpResponse.BodyHandlers.ofString;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+public class HttpVersionsTest {
+
+    SSLContext sslContext;
+    Http2TestServer http2TestServer;
+    Http2TestServer https2TestServer;
+    String http2URI;
+    String https2URI;
+
+    static final int ITERATIONS = 3;
+    static final String[] BODY = new String[] {
+            "I'd like another drink I think",
+            "Another drink to make me pink",
+            "I think I'll drink until I stink",
+            "I'll drink until I cannot blink"
+    };
+    int nextBodyId;
+
+    @DataProvider(name = "scenarios")
+    public Object[][] scenarios() {
+        return new Object[][] {
+                { http2URI,  true  },
+                { https2URI, true  },
+                { http2URI,  false },
+                { https2URI, false },
+        };
+    }
+
+    /** Checks that an HTTP/2 request receives an HTTP/2 response. */
+    @Test(dataProvider = "scenarios")
+    void testHttp2Get(String uri, boolean sameClient) throws Exception {
+        out.println(format("\n--- testHttp2Get uri:%s, sameClient:%s", uri, sameClient));
+        HttpClient client = null;
+        for (int i=0; i<ITERATIONS; i++) {
+            if (!sameClient || client == null)
+                client = HttpClient.newBuilder()
+                                   .sslContext(sslContext)
+                                   .version(HTTP_2)
+                                   .build();
+
+            HttpRequest request = HttpRequest.newBuilder(URI.create(uri))
+                    .build();
+            HttpResponse<String> response = client.send(request, ofString());
+            out.println("Got response: " + response);
+            out.println("Got body: " + response.body());
+
+            assertEquals(response.statusCode(), 200);
+            assertEquals(response.version(), HTTP_2);
+            assertEquals(response.body(), "");
+            if (uri.startsWith("https"))
+                assertTrue(response.sslSession().isPresent());
+        }
+    }
+
+    @Test(dataProvider = "scenarios")
+    void testHttp2Post(String uri, boolean sameClient) throws Exception {
+        out.println(format("\n--- testHttp2Post uri:%s, sameClient:%s", uri, sameClient));
+        HttpClient client = null;
+        for (int i=0; i<ITERATIONS; i++) {
+            if (!sameClient || client == null)
+                client = HttpClient.newBuilder()
+                                   .sslContext(sslContext)
+                                   .version(HTTP_2)
+                                   .build();
+
+            String msg = BODY[nextBodyId++%4];
+            HttpRequest request = HttpRequest.newBuilder(URI.create(uri))
+                    .POST(BodyPublishers.ofString(msg))
+                    .build();
+            HttpResponse<String> response = client.send(request, ofString());
+            out.println("Got response: " + response);
+            out.println("Got body: " + response.body());
+
+            assertEquals(response.statusCode(), 200);
+            assertEquals(response.version(), HTTP_2);
+            assertEquals(response.body(), msg);
+            if (uri.startsWith("https"))
+                assertTrue(response.sslSession().isPresent());
+        }
+    }
+
+    /** Checks that an HTTP/1.1 request receives an HTTP/1.1 response, from the HTTP/2 server. */
+    @Test(dataProvider = "scenarios")
+    void testHttp1dot1Get(String uri, boolean sameClient) throws Exception {
+        out.println(format("\n--- testHttp1dot1Get uri:%s, sameClient:%s", uri, sameClient));
+        HttpClient client = null;
+        for (int i=0; i<ITERATIONS; i++) {
+            if (!sameClient || client == null)
+                client = HttpClient.newBuilder()
+                                   .sslContext(sslContext)
+                                   .version(HTTP_1_1)
+                                   .build();
+
+            HttpRequest request = HttpRequest.newBuilder(URI.create(uri))
+                    .build();
+            HttpResponse<String> response = client.send(request, ofString());
+            out.println("Got response: " + response);
+            out.println("Got body: " + response.body());
+            response.headers().firstValue("X-Received-Body").ifPresent(s -> out.println("X-Received-Body:" + s));
+
+            assertEquals(response.statusCode(), 200);
+            assertEquals(response.version(), HTTP_1_1);
+            assertEquals(response.body(), "");
+            assertEquals(response.headers().firstValue("X-Magic").get(),
+                         "HTTP/1.1 request received by HTTP/2 server");
+            assertEquals(response.headers().firstValue("X-Received-Body").get(), "");
+            if (uri.startsWith("https"))
+                assertTrue(response.sslSession().isPresent());
+        }
+    }
+
+    @Test(dataProvider = "scenarios")
+    void testHttp1dot1Post(String uri, boolean sameClient) throws Exception {
+        out.println(format("\n--- testHttp1dot1Post uri:%s, sameClient:%s", uri, sameClient));
+        HttpClient client = null;
+        for (int i=0; i<ITERATIONS; i++) {
+            if (!sameClient || client == null)
+                client = HttpClient.newBuilder()
+                                   .sslContext(sslContext)
+                                   .version(HTTP_1_1)
+                                   .build();
+            String msg = BODY[nextBodyId++%4];
+            HttpRequest request = HttpRequest.newBuilder(URI.create(uri))
+                    .POST(BodyPublishers.ofString(msg))
+                    .build();
+            HttpResponse<String> response = client.send(request, ofString());
+            out.println("Got response: " + response);
+            out.println("Got body: " + response.body());
+            response.headers().firstValue("X-Received-Body").ifPresent(s -> out.println("X-Received-Body:" + s));
+
+            assertEquals(response.statusCode(), 200);
+            assertEquals(response.version(), HTTP_1_1);
+            assertEquals(response.body(), "");
+            assertEquals(response.headers().firstValue("X-Magic").get(),
+                         "HTTP/1.1 request received by HTTP/2 server");
+            assertEquals(response.headers().firstValue("X-Received-Body").get(), msg);
+            if (uri.startsWith("https"))
+                assertTrue(response.sslSession().isPresent());
+        }
+    }
+
+    // -- Infrastructure
+
+    static final ExecutorService executor = Executors.newCachedThreadPool();
+
+    @BeforeTest
+    public void setup() throws Exception {
+        sslContext = new SimpleSSLContext().get();
+        if (sslContext == null)
+            throw new AssertionError("Unexpected null sslContext");
+
+        http2TestServer =  new Http2TestServer("localhost", false, 0, executor, 50, null, null, true);
+        http2TestServer.addHandler(new Http2VerEchoHandler(), "/http2/vts");
+        http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/vts";
+
+        https2TestServer =  new Http2TestServer("localhost", true, 0, executor, 50, null, sslContext, true);
+        https2TestServer.addHandler(new Http2VerEchoHandler(), "/https2/vts");
+        https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/vts";
+
+        http2TestServer.start();
+        https2TestServer.start();
+    }
+
+    @AfterTest
+    public void teardown() throws Exception {
+        http2TestServer.stop();
+        https2TestServer.stop();
+        executor.shutdown();
+    }
+
+    static class Http2VerEchoHandler implements Http2Handler {
+        @Override
+        public void handle(Http2TestExchange t) throws IOException {
+            try (InputStream is = t.getRequestBody();
+                 OutputStream os = t.getResponseBody()) {
+                byte[] bytes = is.readAllBytes();
+                t.sendResponseHeaders(200, bytes.length);
+                os.write(bytes);
+            }
+        }
+    }
+}
--- a/test/jdk/java/net/httpclient/http2/server/Http2TestServer.java	Fri Apr 03 07:16:35 2020 +0100
+++ b/test/jdk/java/net/httpclient/http2/server/Http2TestServer.java	Fri Apr 03 07:27:53 2020 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2020, 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
@@ -46,6 +46,7 @@
  */
 public class Http2TestServer implements AutoCloseable {
     final ServerSocket server;
+    final boolean supportsHTTP11;
     volatile boolean secure;
     final ExecutorService exec;
     volatile boolean stopping = false;
@@ -111,19 +112,6 @@
         this(serverName, secure, port, exec, 50, null, context);
     }
 
-    /**
-     * Create a Http2Server listening on the given port. Currently needs
-     * to know in advance whether incoming connections are plain TCP "h2c"
-     * or TLS "h2"/
-     *
-     * @param serverName SNI servername
-     * @param secure https or http
-     * @param port listen port
-     * @param exec executor service (cached thread pool is used if null)
-     * @param backlog the server socket backlog
-     * @param properties additional configuration properties
-     * @param context the SSLContext used when secure is true
-     */
     public Http2TestServer(String serverName,
                            boolean secure,
                            int port,
@@ -133,7 +121,43 @@
                            SSLContext context)
         throws Exception
     {
+        this(serverName, secure, port, exec, backlog, properties, context, false);
+    }
+
+    /**
+     * Create a Http2Server listening on the given port. Currently needs
+     * to know in advance whether incoming connections are plain TCP "h2c"
+     * or TLS "h2".
+     *
+     * The HTTP/1.1 support, when supportsHTTP11 is true, is currently limited
+     * to a canned 0-length response that contains the following headers:
+     *       "X-Magic", "HTTP/1.1 request received by HTTP/2 server",
+     *       "X-Received-Body", <the request body>);
+     *
+     * @param serverName SNI servername
+     * @param secure https or http
+     * @param port listen port
+     * @param exec executor service (cached thread pool is used if null)
+     * @param backlog the server socket backlog
+     * @param properties additional configuration properties
+     * @param context the SSLContext used when secure is true
+     * @param supportsHTTP11 if true, the server may issue an HTTP/1.1 response
+     *        to either 1) a non-Upgrade HTTP/1.1 request, or 2) a secure
+     *        connection without the h2 ALPN. Otherwise, false to operate in
+     *        HTTP/2 mode exclusively.
+     */
+    public Http2TestServer(String serverName,
+                           boolean secure,
+                           int port,
+                           ExecutorService exec,
+                           int backlog,
+                           Properties properties,
+                           SSLContext context,
+                           boolean supportsHTTP11)
+        throws Exception
+    {
         this.serverName = serverName;
+        this.supportsHTTP11 = supportsHTTP11;
         if (secure) {
            if (context != null)
                this.sslContext = context;
@@ -220,7 +244,11 @@
         SSLServerSocket se = (SSLServerSocket) fac.createServerSocket();
         se.setReuseAddress(false);
         se.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), backlog);
-        sslp.setApplicationProtocols(new String[]{"h2"});
+        if (supportsHTTP11) {
+            sslp.setApplicationProtocols(new String[]{"h2", "http/1.1"});
+        } else {
+            sslp.setApplicationProtocols(new String[]{"h2"});
+        }
         sslp.setEndpointIdentificationAlgorithm("HTTPS");
         se.setSSLParameters(sslp);
         se.setEnabledCipherSuites(se.getSupportedCipherSuites());
--- a/test/jdk/java/net/httpclient/http2/server/Http2TestServerConnection.java	Fri Apr 03 07:16:35 2020 +0100
+++ b/test/jdk/java/net/httpclient/http2/server/Http2TestServerConnection.java	Fri Apr 03 07:27:53 2020 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2020, 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
@@ -47,6 +47,7 @@
 import jdk.internal.net.http.hpack.Encoder;
 import sun.net.www.http.ChunkedInputStream;
 import sun.net.www.http.HttpClient;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static jdk.internal.net.http.frame.SettingsFrame.HEADER_TABLE_SIZE;
 
 /**
@@ -123,10 +124,15 @@
                               Properties properties)
         throws IOException
     {
+        System.err.println("TestServer: New connection from " + socket);
+
         if (socket instanceof SSLSocket) {
-            handshake(server.serverName(), (SSLSocket)socket);
+            SSLSocket sslSocket = (SSLSocket)socket;
+            handshake(server.serverName(), sslSocket);
+            if (!server.supportsHTTP11 && !"h2".equals(sslSocket.getApplicationProtocol())) {
+                throw new IOException("Unexpected ALPN: [" + sslSocket.getApplicationProtocol() + "]");
+            }
         }
-        System.err.println("TestServer: New connection from " + socket);
         this.server = server;
         this.exchangeSupplier = exchangeSupplier;
         this.streams = Collections.synchronizedMap(new HashMap<>());
@@ -248,7 +254,7 @@
 
     private static void handshake(String name, SSLSocket sock) throws IOException {
         if (name == null) {
-            // no name set. No need to check
+            sock.getSession(); // awaits handshake completion
             return;
         } else if (name.equals("localhost")) {
             name = "localhost";
@@ -304,8 +310,7 @@
         }
     }
 
-    Http1InitialRequest doUpgrade() throws IOException {
-        Http1InitialRequest upgrade = readHttp1Request();
+    Http1InitialRequest doUpgrade(Http1InitialRequest upgrade) throws IOException {
         String h2c = getHeader(upgrade.headers, "Upgrade");
         if (h2c == null || !h2c.equals("h2c")) {
             System.err.println("Server:HEADERS: " + upgrade);
@@ -351,19 +356,58 @@
         return clientSettings.getParameter(SettingsFrame.MAX_FRAME_SIZE);
     }
 
+    /** Sends a pre-canned HTTP/1.1 response. */
+    private void standardHTTP11Response(Http1InitialRequest request)
+        throws IOException
+    {
+        String upgradeHeader = getHeader(request.headers, "Upgrade");
+        if (upgradeHeader != null) {
+            throw new IOException("Unexpected Upgrade header:" + upgradeHeader);
+        }
+
+        sendHttp1Response(200, "OK",
+                          "Connection", "close",
+                          "Content-Length", "0",
+                          "X-Magic", "HTTP/1.1 request received by HTTP/2 server",
+                          "X-Received-Body", new String(request.body, UTF_8));
+    }
+
     void run() throws Exception {
         Http1InitialRequest upgrade = null;
         if (!secure) {
-            upgrade = doUpgrade();
-        } else {
-            readPreface();
-            sendSettingsFrame(true);
-            clientSettings = (SettingsFrame) readFrame();
-            if (clientSettings.getFlag(SettingsFrame.ACK)) {
-                // we received the ack to our frame first
+            Http1InitialRequest request = readHttp1Request();
+            String h2c = getHeader(request.headers, "Upgrade");
+            if (h2c == null || !h2c.equals("h2c")) {
+                if (server.supportsHTTP11) {
+                    standardHTTP11Response(request);
+                    socket.close();
+                    return;
+                } else {
+                    System.err.println("Server:HEADERS: " + upgrade);
+                    throw new IOException("Bad upgrade 1 " + h2c);
+                }
+            }
+            upgrade = doUpgrade(request);
+        } else { // secure
+            SSLSocket sslSocket = (SSLSocket)socket;
+            if (sslSocket.getApplicationProtocol().equals("h2")) {
+                readPreface();
+                sendSettingsFrame(true);
                 clientSettings = (SettingsFrame) readFrame();
+                if (clientSettings.getFlag(SettingsFrame.ACK)) {
+                    // we received the ack to our frame first
+                    clientSettings = (SettingsFrame) readFrame();
+                }
+                nextstream = 1;
+            } else if (sslSocket.getApplicationProtocol().equals("http/1.1") ||
+                       sslSocket.getApplicationProtocol().equals("")) {
+                standardHTTP11Response(readHttp1Request());
+                socket.shutdownOutput();
+                socket.close();
+                return;
+            } else {
+                throw new IOException("Unexpected ALPN:" + sslSocket.getApplicationProtocol());
             }
-            nextstream = 1;
         }
 
         // Uncomment if needed, but very noisy