8025710: Proxied HTTPS connections reused by HttpClient can send CONNECT to the server
Reviewed-by: chegar
Contributed-by: Steven Lawrance <slawrance@salesforce.com>, Andreas Rieber <rieberandreas@gmail.com>
--- a/jdk/src/share/classes/sun/net/www/http/HttpClient.java Wed Apr 23 12:26:13 2014 +0100
+++ b/jdk/src/share/classes/sun/net/www/http/HttpClient.java Wed Apr 23 13:19:21 2014 +0100
@@ -665,7 +665,9 @@
// try once more
openServer();
if (needsTunneling()) {
+ MessageHeader origRequests = requests;
httpuc.doTunneling();
+ requests = origRequests;
}
afterConnect();
writeRequests(requests, poster);
@@ -776,7 +778,9 @@
cachedHttpClient = false;
openServer();
if (needsTunneling()) {
+ MessageHeader origRequests = requests;
httpuc.doTunneling();
+ requests = origRequests;
}
afterConnect();
writeRequests(requests, poster);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/sun/net/www/http/HttpClient/B8025710.java Wed Apr 23 13:19:21 2014 +0100
@@ -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<Thread> 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();
+ }
+}