blob: 0c3a934445905e9d3a3deb6ad6151abf063bc041 [file] [log] [blame]
/*
* 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
* @run main/othervm B8025710
*/
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();
}
}