diff -r b7aa58d7f5aa -r 2b13d126a2d8 test/jdk/java/net/httpclient/HttpRedirectTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/net/httpclient/HttpRedirectTest.java Wed Oct 23 15:54:39 2019 +0100 @@ -0,0 +1,457 @@ +/* + * Copyright (c) 2019, 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. + */ +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsServer; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.AfterClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import static org.testng.Assert.*; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * @test + * @bug 8232625 + * @summary This test verifies that the HttpClient works correctly when redirecting a post request. + * @library /test/lib http2/server + * @build jdk.test.lib.net.SimpleSSLContext HttpServerAdapters DigestEchoServer HttpRedirectTest + * @modules 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 + * java.base/sun.net.www.http + * java.base/sun.net.www + * java.base/sun.net + * @run testng/othervm -Dtest.requiresHost=true + * -Djdk.httpclient.HttpClient.log=headers + * -Djdk.internal.httpclient.debug=false + * HttpRedirectTest + * + */ +public class HttpRedirectTest implements HttpServerAdapters { + static final String GET_RESPONSE_BODY = "Lorem ipsum dolor sit amet"; + static final String REQUEST_BODY = "Here it goes"; + static final SSLContext context; + static { + try { + context = new SimpleSSLContext().get(); + SSLContext.setDefault(context); + } catch (Exception x) { + throw new ExceptionInInitializerError(x); + } + } + + final AtomicLong requestCounter = new AtomicLong(); + final AtomicLong responseCounter = new AtomicLong(); + HttpTestServer http1Server; + HttpTestServer http2Server; + HttpTestServer https1Server; + HttpTestServer https2Server; + DigestEchoServer.TunnelingProxy proxy; + + URI http1URI; + URI https1URI; + URI http2URI; + URI https2URI; + InetSocketAddress proxyAddress; + ProxySelector proxySelector; + HttpClient client; + List> futures = new CopyOnWriteArrayList<>(); + Set pending = new CopyOnWriteArraySet<>(); + + final ExecutorService executor = new ThreadPoolExecutor(12, 60, 10, + TimeUnit.SECONDS, new LinkedBlockingQueue<>()); // Shared by HTTP/1.1 servers + final ExecutorService clientexec = new ThreadPoolExecutor(6, 12, 1, + TimeUnit.SECONDS, new LinkedBlockingQueue<>()); // Used by the client + + public HttpClient newHttpClient(ProxySelector ps) { + HttpClient.Builder builder = HttpClient + .newBuilder() + .sslContext(context) + .executor(clientexec) + .followRedirects(HttpClient.Redirect.ALWAYS) + .proxy(ps); + return builder.build(); + } + + @DataProvider(name="uris") + Object[][] testURIs() throws URISyntaxException { + List uris = List.of( + http1URI.resolve("direct/orig/"), + https1URI.resolve("direct/orig/"), + https1URI.resolve("proxy/orig/"), + http2URI.resolve("direct/orig/"), + https2URI.resolve("direct/orig/"), + https2URI.resolve("proxy/orig/")); + List> redirects = List.of( + Map.entry(301, "GET"), + Map.entry(308, "POST"), + Map.entry(302, "GET"), + Map.entry(303, "GET"), + Map.entry(307, "POST"), + Map.entry(300, "DO_NOT_FOLLOW"), + Map.entry(304, "DO_NOT_FOLLOW"), + Map.entry(305, "DO_NOT_FOLLOW"), + Map.entry(306, "DO_NOT_FOLLOW"), + Map.entry(309, "DO_NOT_FOLLOW"), + Map.entry(new Random().nextInt(90) + 310, "DO_NOT_FOLLOW") + ); + Object[][] tests = new Object[redirects.size() * uris.size()][3]; + int count = 0; + for (int i=0; i < uris.size(); i++) { + URI u = uris.get(i); + for (int j=0; j < redirects.size() ; j++) { + int code = redirects.get(j).getKey(); + String m = redirects.get(j).getValue(); + tests[count][0] = u.resolve(code +"/"); + tests[count][1] = code; + tests[count][2] = m; + count++; + } + } + return tests; + } + + @BeforeClass + public void setUp() throws Exception { + try { + InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); + + // HTTP/1.1 + HttpServer server1 = HttpServer.create(sa, 0); + server1.setExecutor(executor); + http1Server = HttpTestServer.of(server1); + http1Server.addHandler(new HttpTestRedirectHandler("http", http1Server), + "/HttpRedirectTest/http1/"); + http1Server.start(); + http1URI = new URI("http://" + http1Server.serverAuthority() + "/HttpRedirectTest/http1/"); + + + // HTTPS/1.1 + HttpsServer sserver1 = HttpsServer.create(sa, 100); + sserver1.setExecutor(executor); + sserver1.setHttpsConfigurator(new HttpsConfigurator(context)); + https1Server = HttpTestServer.of(sserver1); + https1Server.addHandler(new HttpTestRedirectHandler("https", https1Server), + "/HttpRedirectTest/https1/"); + https1Server.start(); + https1URI = new URI("https://" + https1Server.serverAuthority() + "/HttpRedirectTest/https1/"); + + // HTTP/2.0 + http2Server = HttpTestServer.of( + new Http2TestServer("localhost", false, 0)); + http2Server.addHandler(new HttpTestRedirectHandler("http", http2Server), + "/HttpRedirectTest/http2/"); + http2Server.start(); + http2URI = new URI("http://" + http2Server.serverAuthority() + "/HttpRedirectTest/http2/"); + + // HTTPS/2.0 + https2Server = HttpTestServer.of( + new Http2TestServer("localhost", true, 0)); + https2Server.addHandler(new HttpTestRedirectHandler("https", https2Server), + "/HttpRedirectTest/https2/"); + https2Server.start(); + https2URI = new URI("https://" + https2Server.serverAuthority() + "/HttpRedirectTest/https2/"); + + proxy = DigestEchoServer.createHttpsProxyTunnel( + DigestEchoServer.HttpAuthSchemeType.NONE); + proxyAddress = proxy.getProxyAddress(); + proxySelector = new HttpProxySelector(proxyAddress); + client = newHttpClient(proxySelector); + System.out.println("Setup: done"); + } catch (Exception x) { + tearDown(); throw x; + } catch (Error e) { + tearDown(); throw e; + } + } + + private void testNonIdempotent(URI u, HttpRequest request, + int code, String method) throws Exception { + System.out.println("Testing with " + u); + CompletableFuture> respCf = + client.sendAsync(request, HttpResponse.BodyHandlers.ofString()); + HttpResponse resp = respCf.join(); + if (method.equals("DO_NOT_FOLLOW")) { + assertEquals(resp.statusCode(), code, u + ": status code"); + } else { + assertEquals(resp.statusCode(), 200, u + ": status code"); + } + if (method.equals("POST")) { + assertEquals(resp.body(), REQUEST_BODY, u + ": body"); + } else if (code == 304) { + assertEquals(resp.body(), "", u + ": body"); + } else if (method.equals("DO_NOT_FOLLOW")) { + assertNotEquals(resp.body(), GET_RESPONSE_BODY, u + ": body"); + assertNotEquals(resp.body(), REQUEST_BODY, u + ": body"); + } else { + assertEquals(resp.body(), GET_RESPONSE_BODY, u + ": body"); + } + } + + public void testIdempotent(URI u, HttpRequest request, + int code, String method) throws Exception { + CompletableFuture> respCf = + client.sendAsync(request, HttpResponse.BodyHandlers.ofString()); + HttpResponse resp = respCf.join(); + if (method.equals("DO_NOT_FOLLOW")) { + assertEquals(resp.statusCode(), code, u + ": status code"); + } else { + assertEquals(resp.statusCode(), 200, u + ": status code"); + } + if (method.equals("POST")) { + assertEquals(resp.body(), REQUEST_BODY, u + ": body"); + } else if (code == 304) { + assertEquals(resp.body(), "", u + ": body"); + } else if (method.equals("DO_NOT_FOLLOW")) { + assertNotEquals(resp.body(), GET_RESPONSE_BODY, u + ": body"); + assertNotEquals(resp.body(), REQUEST_BODY, u + ": body"); + } else if (code == 303) { + assertEquals(resp.body(), GET_RESPONSE_BODY, u + ": body"); + } else { + assertEquals(resp.body(), REQUEST_BODY, u + ": body"); + } + } + + @Test(dataProvider = "uris") + public void testPOST(URI uri, int code, String method) throws Exception { + URI u = uri.resolve("foo?n=" + requestCounter.incrementAndGet()); + HttpRequest request = HttpRequest.newBuilder(u) + .POST(HttpRequest.BodyPublishers.ofString(REQUEST_BODY)).build(); + // POST is not considered idempotent. + testNonIdempotent(u, request, code, method); + } + + @Test(dataProvider = "uris") + public void testPUT(URI uri, int code, String method) throws Exception { + URI u = uri.resolve("foo?n=" + requestCounter.incrementAndGet()); + System.out.println("Testing with " + u); + HttpRequest request = HttpRequest.newBuilder(u) + .PUT(HttpRequest.BodyPublishers.ofString(REQUEST_BODY)).build(); + // PUT is considered idempotent. + testIdempotent(u, request, code, method); + } + + @Test(dataProvider = "uris") + public void testFoo(URI uri, int code, String method) throws Exception { + URI u = uri.resolve("foo?n=" + requestCounter.incrementAndGet()); + System.out.println("Testing with " + u); + HttpRequest request = HttpRequest.newBuilder(u) + .method("FOO", + HttpRequest.BodyPublishers.ofString(REQUEST_BODY)).build(); + // FOO is considered idempotent. + testIdempotent(u, request, code, method); + } + + @Test(dataProvider = "uris") + public void testGet(URI uri, int code, String method) throws Exception { + URI u = uri.resolve("foo?n=" + requestCounter.incrementAndGet()); + System.out.println("Testing with " + u); + HttpRequest request = HttpRequest.newBuilder(u) + .method("GET", + HttpRequest.BodyPublishers.ofString(REQUEST_BODY)).build(); + CompletableFuture> respCf = + client.sendAsync(request, HttpResponse.BodyHandlers.ofString()); + HttpResponse resp = respCf.join(); + // body will be preserved except for 304 and 303: this is a GET. + if (method.equals("DO_NOT_FOLLOW")) { + assertEquals(resp.statusCode(), code, u + ": status code"); + } else { + assertEquals(resp.statusCode(), 200, u + ": status code"); + } + if (code == 304) { + assertEquals(resp.body(), "", u + ": body"); + } else if (method.equals("DO_NOT_FOLLOW")) { + assertNotEquals(resp.body(), GET_RESPONSE_BODY, u + ": body"); + assertNotEquals(resp.body(), REQUEST_BODY, u + ": body"); + } else if (code == 303) { + assertEquals(resp.body(), GET_RESPONSE_BODY, u + ": body"); + } else { + assertEquals(resp.body(), REQUEST_BODY, u + ": body"); + } + } + + @AfterClass + public void tearDown() { + proxy = stop(proxy, DigestEchoServer.TunnelingProxy::stop); + http1Server = stop(http1Server, HttpTestServer::stop); + https1Server = stop(https1Server, HttpTestServer::stop); + http2Server = stop(http2Server, HttpTestServer::stop); + https2Server = stop(https2Server, HttpTestServer::stop); + client = null; + try { + executor.awaitTermination(2000, TimeUnit.MILLISECONDS); + } catch (Throwable x) { + } finally { + executor.shutdownNow(); + } + try { + clientexec.awaitTermination(2000, TimeUnit.MILLISECONDS); + } catch (Throwable x) { + } finally { + clientexec.shutdownNow(); + } + System.out.println("Teardown: done"); + } + + private interface Stoppable { public void stop(T service) throws Exception; } + + static T stop(T service, Stoppable stop) { + try { if (service != null) stop.stop(service); } catch (Throwable x) { }; + return null; + } + + static class HttpProxySelector extends ProxySelector { + private static final List NO_PROXY = List.of(Proxy.NO_PROXY); + private final List proxyList; + HttpProxySelector(InetSocketAddress proxyAddress) { + proxyList = List.of(new Proxy(Proxy.Type.HTTP, proxyAddress)); + } + + @Override + public List select(URI uri) { + // our proxy only supports tunneling + if (uri.getScheme().equalsIgnoreCase("https")) { + if (uri.getPath().contains("/proxy/")) { + return proxyList; + } + } + return NO_PROXY; + } + + @Override + public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { + System.err.println("Connection to proxy failed: " + ioe); + System.err.println("Proxy: " + sa); + System.err.println("\tURI: " + uri); + ioe.printStackTrace(); + } + } + + public static class HttpTestRedirectHandler implements HttpTestHandler { + static final AtomicLong respCounter = new AtomicLong(); + final String scheme; + final HttpTestServer server; + HttpTestRedirectHandler(String scheme, HttpTestServer server) { + this.scheme = scheme; + this.server = server; + } + + @Override + public void handle(HttpTestExchange t) throws IOException { + try (InputStream is = t.getRequestBody()) { + byte[] bytes = is.readAllBytes(); + URI u = t.getRequestURI(); + long responseID = Long.parseLong(u.getQuery().substring(2)); + String path = u.getPath(); + int i = path.lastIndexOf('/'); + String file = path.substring(i+1); + String parent = path.substring(0, i); + int code = 200; + if (file.equals("foo")) { + i = parent.lastIndexOf("/"); + code = Integer.parseInt(parent.substring(i+1)); + } + String response; + if (code == 200) { + if (t.getRequestMethod().equals("GET")) { + if (bytes.length == 0) { + response = GET_RESPONSE_BODY; + } else { + response = new String(bytes, StandardCharsets.UTF_8); + } + } else if (t.getRequestMethod().equals("POST")) { + response = new String(bytes, StandardCharsets.UTF_8); + } else { + response = new String(bytes, StandardCharsets.UTF_8); + } + } else if (code < 300 || code > 399) { + response = "Unexpected code: " + code; + code = 400; + } else { + try { + URI reloc = new URI(scheme, server.serverAuthority(), parent + "/bar", u.getQuery(), null); + t.getResponseHeaders().addHeader("Location", reloc.toASCIIString()); + if (code != 304) { + response = "Code: " + code; + } else response = null; + } catch (URISyntaxException x) { + x.printStackTrace(); + x.printStackTrace(System.out); + code = 400; + response = x.toString(); + } + } + + System.out.println("Server " + t.getRequestURI() + " sending response " + responseID); + System.out.println("code: " + code + " body: " + response); + t.sendResponseHeaders(code, code == 304 ? 0: -1); + if (code != 304) { + try (OutputStream os = t.getResponseBody()) { + bytes = response.getBytes(StandardCharsets.UTF_8); + os.write(bytes); + os.flush(); + } + } else { + bytes = new byte[0]; + } + + System.out.println("\tresp:" + responseID + ": wrote " + bytes.length + " bytes"); + } catch (Throwable e) { + e.printStackTrace(); + e.printStackTrace(System.out); + throw e; + } + } + } + +}