| /* |
| * 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(); |
| } |
| } |