diff --git a/jdk/src/share/classes/sun/net/www/http/HttpClient.java b/jdk/src/share/classes/sun/net/www/http/HttpClient.java index 44a52530752..3013da01df3 100644 --- a/jdk/src/share/classes/sun/net/www/http/HttpClient.java +++ b/jdk/src/share/classes/sun/net/www/http/HttpClient.java @@ -665,7 +665,9 @@ public class HttpClient extends NetworkClient { // try once more openServer(); if (needsTunneling()) { + MessageHeader origRequests = requests; httpuc.doTunneling(); + requests = origRequests; } afterConnect(); writeRequests(requests, poster); @@ -776,7 +778,9 @@ public class HttpClient extends NetworkClient { cachedHttpClient = false; openServer(); if (needsTunneling()) { + MessageHeader origRequests = requests; httpuc.doTunneling(); + requests = origRequests; } afterConnect(); writeRequests(requests, poster); diff --git a/jdk/test/sun/net/www/http/HttpClient/B8025710.java b/jdk/test/sun/net/www/http/HttpClient/B8025710.java new file mode 100644 index 00000000000..8074c761188 --- /dev/null +++ b/jdk/test/sun/net/www/http/HttpClient/B8025710.java @@ -0,0 +1,409 @@ +/* + * Copyright (c) 2014, 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 java.io.*; +import java.net.*; +import java.security.*; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.net.ServerSocketFactory; +import javax.net.SocketFactory; +import javax.net.ssl.*; + +/** + * @test + * @bug 8025710 + * @summary Proxied https connection reuse by HttpClient can send CONNECT to the server + */ +public class B8025710 { + + private final static AtomicBoolean connectInServer = new AtomicBoolean(); + private static final String keystorefile = + System.getProperty("test.src", "./") + + "/../../../../../javax/net/ssl/etc/keystore"; + private static final String passphrase = "passphrase"; + + public static void main(String[] args) throws Exception { + new B8025710().runTest(); + + if (connectInServer.get()) + throw new RuntimeException("TEST FAILED: server got proxy header"); + else + System.out.println("TEST PASSED"); + } + + private void runTest() throws Exception { + ProxyServer proxyServer = new ProxyServer(); + HttpServer httpServer = new HttpServer(); + httpServer.start(); + proxyServer.start(); + + URL url = new URL("https", InetAddress.getLocalHost().getHostName(), + httpServer.getPort(), "/"); + + Proxy proxy = new Proxy(Proxy.Type.HTTP, proxyServer.getAddress()); + + HttpsURLConnection.setDefaultSSLSocketFactory(createTestSSLSocketFactory()); + + // Make two connections. The bug occurs when the second request is made + for (int i = 0; i < 2; i++) { + System.out.println("Client: Requesting " + url.toExternalForm() + + " via " + proxy.toString() + + " (attempt " + (i + 1) + " of 2)"); + + HttpsURLConnection connection = + (HttpsURLConnection) url.openConnection(proxy); + + connection.setRequestMethod("POST"); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setRequestProperty("User-Agent", "Test/1.0"); + connection.getOutputStream().write("Hello, world!".getBytes("UTF-8")); + + if (connection.getResponseCode() != 200) { + System.err.println("Client: Unexpected response code " + + connection.getResponseCode()); + break; + } + + String response = readLine(connection.getInputStream()); + if (!"Hi!".equals(response)) { + System.err.println("Client: Unexpected response body: " + + response); + } + } + httpServer.close(); + proxyServer.close(); + httpServer.join(); + proxyServer.join(); + } + + class ProxyServer extends Thread implements Closeable { + + private final ServerSocket proxySocket; + private final Pattern connectLinePattern = + Pattern.compile("^CONNECT ([^: ]+):([0-9]+) HTTP/[0-9.]+$"); + private final String PROXY_RESPONSE = + "HTTP/1.0 200 Connection Established\r\n" + + "Proxy-Agent: TestProxy/1.0\r\n" + + "\r\n"; + + ProxyServer() throws Exception { + super("ProxyServer Thread"); + + // Create the http proxy server socket + proxySocket = ServerSocketFactory.getDefault().createServerSocket(); + proxySocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 0)); + } + + public SocketAddress getAddress() { return proxySocket.getLocalSocketAddress(); } + + @Override + public void close() throws IOException { + proxySocket.close(); + } + + @Override + public void run() { + ArrayList threads = new ArrayList<>(); + int connectionCount = 0; + try { + while (connectionCount++ < 2) { + final Socket clientSocket = proxySocket.accept(); + final int proxyConnectionCount = connectionCount; + System.out.println("Proxy: NEW CONNECTION " + + proxyConnectionCount); + + Thread t = new Thread("ProxySocket" + proxyConnectionCount) { + @Override + public void run() { + try { + String firstLine = + readHeader(clientSocket.getInputStream()); + + Matcher connectLineMatcher = + connectLinePattern.matcher(firstLine); + if (!connectLineMatcher.matches()) { + System.out.println("Proxy: Unexpected" + + " request to the proxy: " + + firstLine); + return; + } + + String host = connectLineMatcher.group(1); + String portStr = connectLineMatcher.group(2); + int port = Integer.parseInt(portStr); + + Socket serverSocket = SocketFactory.getDefault() + .createSocket(host, port); + + clientSocket.getOutputStream() + .write(PROXY_RESPONSE.getBytes("UTF-8")); + + ProxyTunnel copyToClient = + new ProxyTunnel(serverSocket, clientSocket); + ProxyTunnel copyToServer = + new ProxyTunnel(clientSocket, serverSocket); + + copyToClient.start(); + copyToServer.start(); + + copyToClient.join(); + // here copyToClient.close() would not provoke the + // bug ( since it would trigger the retry logic in + // HttpURLConnction.writeRequests ), so close only + // the output to get the connection in this state. + clientSocket.shutdownOutput(); + + try { + Thread.sleep(3000); + } catch (InterruptedException ignored) { } + + // now close all connections to finish the test + copyToServer.close(); + copyToClient.close(); + } catch (IOException | NumberFormatException + | InterruptedException e) { + e.printStackTrace(); + } + } + }; + threads.add(t); + t.start(); + } + for (Thread t: threads) + t.join(); + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } + } + + /** + * This inner class provides unidirectional data flow through the sockets + * by continuously copying bytes from the input socket onto the output + * socket, until both sockets are open and EOF has not been received. + */ + class ProxyTunnel extends Thread { + private final Socket sockIn; + private final Socket sockOut; + private final InputStream input; + private final OutputStream output; + + public ProxyTunnel(Socket sockIn, Socket sockOut) throws IOException { + super("ProxyTunnel"); + this.sockIn = sockIn; + this.sockOut = sockOut; + input = sockIn.getInputStream(); + output = sockOut.getOutputStream(); + } + + public void run() { + byte[] buf = new byte[8192]; + int bytesRead; + + try { + while ((bytesRead = input.read(buf)) >= 0) { + output.write(buf, 0, bytesRead); + output.flush(); + } + } catch (IOException ignored) { + close(); + } + } + + public void close() { + try { + if (!sockIn.isClosed()) + sockIn.close(); + if (!sockOut.isClosed()) + sockOut.close(); + } catch (IOException ignored) { } + } + } + + /** + * the server thread + */ + class HttpServer extends Thread implements Closeable { + + private final ServerSocket serverSocket; + private final SSLSocketFactory sslSocketFactory; + private final String serverResponse = + "HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain\r\n" + + "Content-Length: 3\r\n" + + "\r\n" + + "Hi!"; + private int connectionCount = 0; + + HttpServer() throws Exception { + super("HttpServer Thread"); + + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(new FileInputStream(keystorefile), passphrase.toCharArray()); + KeyManagerFactory factory = KeyManagerFactory.getInstance("SunX509"); + factory.init(ks, passphrase.toCharArray()); + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(factory.getKeyManagers(), null, null); + + sslSocketFactory = ctx.getSocketFactory(); + + // Create the server that the test wants to connect to via the proxy + serverSocket = ServerSocketFactory.getDefault().createServerSocket(); + serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 0)); + } + + public int getPort() { return serverSocket.getLocalPort(); } + + @Override + public void close() throws IOException { serverSocket.close(); } + + @Override + public void run() { + try { + while (connectionCount++ < 2) { + Socket socket = serverSocket.accept(); + System.out.println("Server: NEW CONNECTION " + + connectionCount); + + SSLSocket sslSocket = (SSLSocket) sslSocketFactory + .createSocket(socket,null, getPort(), false); + sslSocket.setUseClientMode(false); + sslSocket.startHandshake(); + + String firstLine = readHeader(sslSocket.getInputStream()); + if (firstLine != null && firstLine.contains("CONNECT")) { + System.out.println("Server: BUG! HTTP CONNECT" + + " encountered: " + firstLine); + connectInServer.set(true); + } + + // write the success response, the request body is not read. + // close only output and keep input open. + OutputStream out = sslSocket.getOutputStream(); + out.write(serverResponse.getBytes("UTF-8")); + socket.shutdownOutput(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * read the header and return only the first line. + * + * @param inputStream the stream to read from + * @return the first line of the stream + * @throws IOException if reading failed + */ + private static String readHeader(InputStream inputStream) + throws IOException { + String line; + String firstLine = null; + while ((line = readLine(inputStream)) != null && line.length() > 0) { + if (firstLine == null) { + firstLine = line; + } + } + + return firstLine; + } + + /** + * read a line from stream. + * + * @param inputStream the stream to read from + * @return the line + * @throws IOException if reading failed + */ + private static String readLine(InputStream inputStream) + throws IOException { + final StringBuilder line = new StringBuilder(); + int ch; + while ((ch = inputStream.read()) != -1) { + if (ch == '\r') { + continue; + } + + if (ch == '\n') { + break; + } + + line.append((char) ch); + } + + return line.toString(); + } + + private SSLSocketFactory createTestSSLSocketFactory() { + + HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession sslSession) { + // ignore the cert's CN; it's not important to this test + return true; + } + }); + + // Set up the socket factory to use a trust manager that trusts all + // certs, since trust validation isn't important to this test + final TrustManager[] trustAllCertChains = new TrustManager[] { + new X509TrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + @Override + public void checkClientTrusted(X509Certificate[] certs, + String authType) { + } + + @Override + public void checkServerTrusted(X509Certificate[] certs, + String authType) { + } + } + }; + + final SSLContext sc; + try { + sc = SSLContext.getInstance("TLS"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + + try { + sc.init(null, trustAllCertChains, new java.security.SecureRandom()); + } catch (KeyManagementException e) { + throw new RuntimeException(e); + } + + return sc.getSocketFactory(); + } +}