test/jdk/java/net/httpclient/HttpRedirectTest.java
author dfuchs
Wed, 23 Oct 2019 15:54:39 +0100
changeset 58758 2b13d126a2d8
permissions -rw-r--r--
8232625: HttpClient redirect policy should be more conservative Summary: When enabled, HttpClient redirect is fixed to drop the body when the request method is changed, and to relay any redirection code it does not understand to the caller. Reviewed-by: chegar

/*
 * 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<CompletableFuture<?>>  futures = new CopyOnWriteArrayList<>();
    Set<URI> 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<URI> 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<Map.Entry<Integer, String>> 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<HttpResponse<String>> respCf =
                client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
        HttpResponse<String> 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<HttpResponse<String>> respCf =
                client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
        HttpResponse<String> 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<HttpResponse<String>> respCf =
                client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
        HttpResponse<String> 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<T> { public void stop(T service) throws Exception; }

    static <T>  T stop(T service, Stoppable<T> stop) {
        try { if (service != null) stop.stop(service); } catch (Throwable x) { };
        return null;
    }

    static class HttpProxySelector extends ProxySelector {
        private static final List<Proxy> NO_PROXY = List.of(Proxy.NO_PROXY);
        private final List<Proxy> proxyList;
        HttpProxySelector(InetSocketAddress proxyAddress) {
            proxyList = List.of(new Proxy(Proxy.Type.HTTP, proxyAddress));
        }

        @Override
        public List<Proxy> 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;
            }
        }
    }

}