8025710: Proxied HTTPS connections reused by HttpClient can send CONNECT to the server
Co-authored-by: Andreas Rieber <rieberandreas@gmail.com> Reviewed-by: chegar
This commit is contained in:
parent
2a22218a04
commit
245c97d147
@ -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);
|
||||
|
409
jdk/test/sun/net/www/http/HttpClient/B8025710.java
Normal file
409
jdk/test/sun/net/www/http/HttpClient/B8025710.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user