8209178: Proxied HttpsURLConnection doesn't send BODY when retrying POST request
Summary: Preserve BODY in poster output stream before sending CONNECT request
Reviewed-by: dfuchs, vtewari
--- a/src/java.base/share/classes/sun/net/www/http/HttpClient.java Tue Oct 01 12:10:33 2019 +0100
+++ b/src/java.base/share/classes/sun/net/www/http/HttpClient.java Thu Sep 26 12:35:51 2019 +0100
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 1994, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1994, 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
@@ -707,11 +707,7 @@
} else {
// try once more
openServer();
- if (needsTunneling()) {
- MessageHeader origRequests = requests;
- httpuc.doTunneling();
- requests = origRequests;
- }
+ checkTunneling(httpuc);
afterConnect();
writeRequests(requests, poster);
return parseHTTP(responses, pi, httpuc);
@@ -722,6 +718,18 @@
}
+ // Check whether tunnel must be open and open it if necessary
+ // (in the case of HTTPS with proxy)
+ private void checkTunneling(HttpURLConnection httpuc) throws IOException {
+ if (needsTunneling()) {
+ MessageHeader origRequests = requests;
+ PosterOutputStream origPoster = poster;
+ httpuc.doTunneling();
+ requests = origRequests;
+ poster = origPoster;
+ }
+ }
+
private boolean parseHTTPHeader(MessageHeader responses, ProgressSource pi, HttpURLConnection httpuc)
throws IOException {
/* If "HTTP/*" is found in the beginning, return true. Let
@@ -849,11 +857,7 @@
closeServer();
cachedHttpClient = false;
openServer();
- if (needsTunneling()) {
- MessageHeader origRequests = requests;
- httpuc.doTunneling();
- requests = origRequests;
- }
+ checkTunneling(httpuc);
afterConnect();
writeRequests(requests, poster);
return parseHTTP(responses, pi, httpuc);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/net/www/http/HttpClient/B8209178.java Thu Sep 26 12:35:51 2019 +0100
@@ -0,0 +1,409 @@
+/*
+ * 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.
+ */
+
+/*
+ * @test
+ * @bug 8209178
+ * @modules java.base/sun.net.www java.base/sun.security.x509 java.base/sun.security.tools.keytool
+ * @library /test/lib
+ * @run main/othervm -Dsun.net.http.retryPost=true B8209178
+ * @run main/othervm -Dsun.net.http.retryPost=false B8209178
+ * @summary Proxied HttpsURLConnection doesn't send BODY when retrying POST request
+ */
+
+import java.io.*;
+import java.net.*;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+import javax.net.ssl.*;
+
+import com.sun.net.httpserver.*;
+import jdk.test.lib.net.URIBuilder;
+import sun.security.tools.keytool.CertAndKeyGen;
+import sun.security.x509.X500Name;
+
+public class B8209178 {
+ static {
+ try {
+ HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);
+ SSLContext.setDefault(new TestSSLContext().get());
+ } catch (Exception ex) {
+ throw new ExceptionInInitializerError(ex);
+ }
+ }
+
+ static final String RESPONSE = "<html><body><p>Hello World!</body></html>";
+ static final String PATH = "/foo/";
+ static final String RETRYPOST = System.getProperty("sun.net.http.retryPost");
+
+ static HttpServer createHttpsServer() throws IOException, NoSuchAlgorithmException {
+ HttpsServer server = HttpsServer.create();
+ HttpContext context = server.createContext(PATH);
+ context.setHandler(new HttpHandler() {
+
+ boolean simulateError = true;
+
+ @Override
+ public void handle(HttpExchange he) throws IOException {
+
+ System.out.printf("%s - received request on : %s%n",
+ Thread.currentThread().getName(),
+ he.getRequestURI());
+ System.out.printf("%s - received request headers : %s%n",
+ Thread.currentThread().getName(),
+ new HashMap(he.getRequestHeaders()));
+
+ InputStream requestBody = he.getRequestBody();
+ String body = B8209178.toString(requestBody);
+
+ System.out.printf("%s - received request body : %s%n",
+ Thread.currentThread().getName(), body);
+
+ if (simulateError) {
+ simulateError = false;
+
+ System.out.printf("%s - closing connection unexpectedly ... %n",
+ Thread.currentThread().getName(), he.getRequestHeaders());
+
+ he.close(); // try not to respond anything the first time ...
+ return;
+ }
+
+ he.getResponseHeaders().add("encoding", "UTF-8");
+ he.sendResponseHeaders(200, RESPONSE.length());
+ he.getResponseBody().write(RESPONSE.getBytes(StandardCharsets.UTF_8));
+ he.close();
+ }
+ });
+
+ server.setHttpsConfigurator(new Configurator(SSLContext.getDefault()));
+ server.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
+ return server;
+ }
+
+ public static void main(String[] args) throws IOException, NoSuchAlgorithmException {
+ HttpServer server = createHttpsServer();
+ server.start();
+ try {
+ new B8209178().test(server);
+
+ } finally {
+ server.stop(0);
+ System.out.println("Server stopped");
+ }
+ }
+
+ public void test(HttpServer server /*, HttpClient.Version version*/) throws IOException {
+ System.out.println("System property retryPost: " + RETRYPOST);
+ System.out.println("Server is: " + server.getAddress());
+ System.out.println("Verifying communication with server");
+ URI uri = URIBuilder.newBuilder()
+ .scheme("https")
+ .host(server.getAddress().getAddress())
+ .port(server.getAddress().getPort())
+ .path(PATH + "x")
+ .buildUnchecked();
+
+ TunnelingProxy proxy = new TunnelingProxy(server);
+ proxy.start();
+
+ try {
+ System.out.println("Proxy started");
+ Proxy p = new Proxy(Proxy.Type.HTTP,
+ InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort()));
+ System.out.println("Verifying communication with proxy");
+
+ callHttpsServerThroughProxy(uri, p);
+
+ } finally {
+ System.out.println("Stopping proxy");
+ proxy.stop();
+ System.out.println("Proxy stopped");
+ }
+ }
+
+ private void callHttpsServerThroughProxy(URI uri, Proxy p) throws IOException {
+ HttpsURLConnection urlConnection = (HttpsURLConnection) uri.toURL().openConnection(p);
+
+ urlConnection.setConnectTimeout(1000);
+ urlConnection.setReadTimeout(3000);
+ urlConnection.setDoInput(true);
+ urlConnection.setDoOutput(true);
+ urlConnection.setRequestMethod("POST");
+ urlConnection.setUseCaches(false);
+
+ urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+ urlConnection.setRequestProperty("charset", "utf-8");
+ urlConnection.setRequestProperty("Connection", "keep-alive");
+
+ String urlParameters = "param1=a¶m2=b¶m3=c";
+ byte[] postData = urlParameters.getBytes(StandardCharsets.UTF_8);
+
+ OutputStream outputStream = urlConnection.getOutputStream();
+ outputStream.write(postData);
+ outputStream.close();
+
+ int responseCode;
+
+ try {
+ responseCode = urlConnection.getResponseCode();
+ System.out.printf(" ResponseCode : %s%n", responseCode);
+ String output;
+ InputStream inputStream = (responseCode < 400) ? urlConnection.getInputStream() : urlConnection.getErrorStream();
+ output = toString(inputStream);
+ inputStream.close();
+ System.out.printf(" Output from server : %s%n", output);
+
+ if (responseCode == 200) { // OK !
+ } else {
+ throw new RuntimeException("Bad response Code : " + responseCode);
+ }
+ } catch (SocketException se) {
+ if (RETRYPOST.equals("true")) { // Should not get here with the fix
+ throw new RuntimeException("Unexpected Socket Exception: " + se);
+ } else {
+ System.out.println("Socket Exception received as expected: " + se);
+ }
+ }
+ }
+
+ static class TunnelingProxy {
+ final Thread accept;
+ final ServerSocket ss;
+ final boolean DEBUG = false;
+ final HttpServer serverImpl;
+
+ TunnelingProxy(HttpServer serverImpl) throws IOException {
+ this.serverImpl = serverImpl;
+ ss = new ServerSocket();
+ accept = new Thread(this::accept);
+ }
+
+ void start() throws IOException {
+ ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
+ accept.start();
+ }
+
+ // Pipe the input stream to the output stream
+ private synchronized Thread pipe(InputStream is, OutputStream os, char tag) {
+ return new Thread("TunnelPipe(" + tag + ")") {
+ @Override
+ public void run() {
+ try {
+ try {
+ int c;
+ while ((c = is.read()) != -1) {
+ os.write(c);
+ os.flush();
+ // if DEBUG prints a + or a - for each transferred
+ // character.
+ if (DEBUG) System.out.print(tag);
+ }
+ is.close();
+ } finally {
+ os.close();
+ }
+ } catch (IOException ex) {
+ if (DEBUG) ex.printStackTrace(System.out);
+ }
+ }
+ };
+ }
+
+ public InetSocketAddress getAddress() {
+ return new InetSocketAddress(ss.getInetAddress(), ss.getLocalPort());
+ }
+
+ // 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();
+ }
+
+ public void accept() {
+ Socket clientConnection = null;
+ try {
+ while (true) {
+ System.out.println("Tunnel: Waiting for client");
+ Socket previous = clientConnection;
+ try {
+ clientConnection = ss.accept();
+ } catch (IOException io) {
+ if (DEBUG) io.printStackTrace(System.out);
+ break;
+ } finally {
+ // we have only 1 client at a time, so it is safe
+ // to close the previous connection here
+ if (previous != null) previous.close();
+ }
+ System.out.println("Tunnel: Client accepted");
+ Socket targetConnection = null;
+ InputStream ccis = clientConnection.getInputStream();
+ OutputStream ccos = clientConnection.getOutputStream();
+ Writer w = new OutputStreamWriter(ccos, "UTF-8");
+ PrintWriter pw = new PrintWriter(w);
+ System.out.println("Tunnel: Reading request line");
+ String requestLine = readLine(ccis);
+ System.out.println("Tunnel: Request status line: " + requestLine);
+ if (requestLine.startsWith("CONNECT ")) {
+ // We should probably check that the next word following
+ // CONNECT is the host:port of our HTTPS serverImpl.
+ // Some improvement for a followup!
+
+ // Read all headers until we find the empty line that
+ // signals the end of all headers.
+ while (!requestLine.equals("")) {
+ System.out.println("Tunnel: Reading header: "
+ + (requestLine = readLine(ccis)));
+ }
+
+ // Open target connection
+ targetConnection = new Socket(
+ serverImpl.getAddress().getAddress(),
+ serverImpl.getAddress().getPort());
+
+ // Then send the 200 OK response to the client
+ System.out.println("Tunnel: Sending "
+ + "HTTP/1.1 200 OK\r\n\r\n");
+ pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
+ pw.flush();
+ } else {
+ // This should not happen
+ throw new IOException("Tunnel: Unexpected status line: "
+ + requestLine);
+ }
+
+ // Pipe the input stream of the client connection to the
+ // output stream of the target connection and conversely.
+ // Now the client and target will just talk to each other.
+ System.out.println("Tunnel: Starting tunnel pipes");
+ Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+');
+ Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-');
+ t1.start();
+ t2.start();
+
+ // We have only 1 client... wait until it has finished before
+ // accepting a new connection request.
+ System.out.println("Tunnel: Waiting for pipes to close");
+ t1.join();
+ t2.join();
+ System.out.println("Tunnel: Done - waiting for next client");
+ }
+ } catch (Throwable ex) {
+ try {
+ ss.close();
+ } catch (IOException ex1) {
+ ex.addSuppressed(ex1);
+ }
+ ex.printStackTrace(System.err);
+ }
+ }
+
+ void stop() throws IOException {
+ ss.close();
+ }
+ }
+
+ static class Configurator extends HttpsConfigurator {
+ public Configurator(SSLContext ctx) {
+ super(ctx);
+ }
+
+ @Override
+ public void configure(HttpsParameters params) {
+ params.setSSLParameters(getSSLContext().getSupportedSSLParameters());
+ }
+ }
+
+
+ static class TestSSLContext {
+
+ SSLContext ssl;
+
+ public TestSSLContext() throws Exception {
+ init();
+ }
+
+ private void init() throws Exception {
+
+ CertAndKeyGen keyGen = new CertAndKeyGen("RSA", "SHA1WithRSA", null);
+ keyGen.generate(1024);
+
+ //Generate self signed certificate
+ X509Certificate[] chain = new X509Certificate[1];
+ chain[0] = keyGen.getSelfCertificate(new X500Name("CN=ROOT"), (long) 365 * 24 * 3600);
+
+ char[] passphrase = "passphrase".toCharArray();
+
+ KeyStore ks = KeyStore.getInstance("JKS");
+ ks.load(null, passphrase); // must be "initialized" ...
+
+ ks.setKeyEntry("server", keyGen.getPrivateKey(), passphrase, chain);
+
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
+ kmf.init(ks, passphrase);
+
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
+ tmf.init(ks);
+
+ ssl = SSLContext.getInstance("TLS");
+ ssl.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
+ }
+
+ public SSLContext get() {
+ return ssl;
+ }
+ }
+
+ // ###############################################################################################
+
+ private static String toString(InputStream inputStream) throws IOException {
+ StringBuilder sb = new StringBuilder();
+ BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+ int i = bufferedReader.read();
+ while (i != -1) {
+ sb.append((char) i);
+ i = bufferedReader.read();
+ }
+ bufferedReader.close();
+ return sb.toString();
+ }
+}