8209178: Proxied HttpsURLConnection doesn't send BODY when retrying POST request
authorjboes
Thu, 26 Sep 2019 12:35:51 +0100
changeset 58424 94ca05133eb2
parent 58423 54de0c861d32
child 58425 f4a4804ab3e6
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
src/java.base/share/classes/sun/net/www/http/HttpClient.java
test/jdk/sun/net/www/http/HttpClient/B8209178.java
--- 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&param2=b&param3=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();
+    }
+}