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;
}
}
}
}