1 /* 2 * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 import java.io.*; 25 import java.net.*; 26 import java.security.*; 27 import java.security.cert.X509Certificate; 28 import java.util.ArrayList; 29 import java.util.concurrent.atomic.AtomicBoolean; 30 import java.util.regex.Matcher; 31 import java.util.regex.Pattern; 32 import javax.net.ServerSocketFactory; 33 import javax.net.SocketFactory; 34 import javax.net.ssl.*; 35 36 /** 37 * @test 38 * @bug 8025710 39 * @summary Proxied https connection reuse by HttpClient can send CONNECT to the server 40 * @run main/othervm B8025710 41 */ 42 public class B8025710 { 43 44 private final static AtomicBoolean connectInServer = new AtomicBoolean(); 45 private static final String keystorefile = 46 System.getProperty("test.src", "./") 47 + "/../../../../../javax/net/ssl/etc/keystore"; 48 private static final String passphrase = "passphrase"; 49 main(String[] args)50 public static void main(String[] args) throws Exception { 51 new B8025710().runTest(); 52 53 if (connectInServer.get()) 54 throw new RuntimeException("TEST FAILED: server got proxy header"); 55 else 56 System.out.println("TEST PASSED"); 57 } 58 runTest()59 private void runTest() throws Exception { 60 ProxyServer proxyServer = new ProxyServer(); 61 HttpServer httpServer = new HttpServer(); 62 httpServer.start(); 63 proxyServer.start(); 64 65 URL url = new URL("https", InetAddress.getLocalHost().getHostName(), 66 httpServer.getPort(), "/"); 67 68 Proxy proxy = new Proxy(Proxy.Type.HTTP, proxyServer.getAddress()); 69 70 HttpsURLConnection.setDefaultSSLSocketFactory(createTestSSLSocketFactory()); 71 72 // Make two connections. The bug occurs when the second request is made 73 for (int i = 0; i < 2; i++) { 74 System.out.println("Client: Requesting " + url.toExternalForm() 75 + " via " + proxy.toString() 76 + " (attempt " + (i + 1) + " of 2)"); 77 78 HttpsURLConnection connection = 79 (HttpsURLConnection) url.openConnection(proxy); 80 81 connection.setRequestMethod("POST"); 82 connection.setDoInput(true); 83 connection.setDoOutput(true); 84 connection.setRequestProperty("User-Agent", "Test/1.0"); 85 connection.getOutputStream().write("Hello, world!".getBytes("UTF-8")); 86 87 if (connection.getResponseCode() != 200) { 88 System.err.println("Client: Unexpected response code " 89 + connection.getResponseCode()); 90 break; 91 } 92 93 String response = readLine(connection.getInputStream()); 94 if (!"Hi!".equals(response)) { 95 System.err.println("Client: Unexpected response body: " 96 + response); 97 } 98 } 99 httpServer.close(); 100 proxyServer.close(); 101 httpServer.join(); 102 proxyServer.join(); 103 } 104 105 class ProxyServer extends Thread implements Closeable { 106 107 private final ServerSocket proxySocket; 108 private final Pattern connectLinePattern = 109 Pattern.compile("^CONNECT ([^: ]+):([0-9]+) HTTP/[0-9.]+$"); 110 private final String PROXY_RESPONSE = 111 "HTTP/1.0 200 Connection Established\r\n" 112 + "Proxy-Agent: TestProxy/1.0\r\n" 113 + "\r\n"; 114 ProxyServer()115 ProxyServer() throws Exception { 116 super("ProxyServer Thread"); 117 118 // Create the http proxy server socket 119 proxySocket = ServerSocketFactory.getDefault().createServerSocket(); 120 proxySocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 0)); 121 } 122 getAddress()123 public SocketAddress getAddress() { return proxySocket.getLocalSocketAddress(); } 124 125 @Override close()126 public void close() throws IOException { 127 proxySocket.close(); 128 } 129 130 @Override run()131 public void run() { 132 ArrayList<Thread> threads = new ArrayList<>(); 133 int connectionCount = 0; 134 try { 135 while (connectionCount++ < 2) { 136 final Socket clientSocket = proxySocket.accept(); 137 final int proxyConnectionCount = connectionCount; 138 System.out.println("Proxy: NEW CONNECTION " 139 + proxyConnectionCount); 140 141 Thread t = new Thread("ProxySocket" + proxyConnectionCount) { 142 @Override 143 public void run() { 144 try { 145 String firstLine = 146 readHeader(clientSocket.getInputStream()); 147 148 Matcher connectLineMatcher = 149 connectLinePattern.matcher(firstLine); 150 if (!connectLineMatcher.matches()) { 151 System.out.println("Proxy: Unexpected" 152 + " request to the proxy: " 153 + firstLine); 154 return; 155 } 156 157 String host = connectLineMatcher.group(1); 158 String portStr = connectLineMatcher.group(2); 159 int port = Integer.parseInt(portStr); 160 161 Socket serverSocket = SocketFactory.getDefault() 162 .createSocket(host, port); 163 164 clientSocket.getOutputStream() 165 .write(PROXY_RESPONSE.getBytes("UTF-8")); 166 167 ProxyTunnel copyToClient = 168 new ProxyTunnel(serverSocket, clientSocket); 169 ProxyTunnel copyToServer = 170 new ProxyTunnel(clientSocket, serverSocket); 171 172 copyToClient.start(); 173 copyToServer.start(); 174 175 copyToClient.join(); 176 // here copyToClient.close() would not provoke the 177 // bug ( since it would trigger the retry logic in 178 // HttpURLConnction.writeRequests ), so close only 179 // the output to get the connection in this state. 180 clientSocket.shutdownOutput(); 181 182 try { 183 Thread.sleep(3000); 184 } catch (InterruptedException ignored) { } 185 186 // now close all connections to finish the test 187 copyToServer.close(); 188 copyToClient.close(); 189 } catch (IOException | NumberFormatException 190 | InterruptedException e) { 191 e.printStackTrace(); 192 } 193 } 194 }; 195 threads.add(t); 196 t.start(); 197 } 198 for (Thread t: threads) 199 t.join(); 200 } catch (IOException | InterruptedException e) { 201 e.printStackTrace(); 202 } 203 } 204 } 205 206 /** 207 * This inner class provides unidirectional data flow through the sockets 208 * by continuously copying bytes from the input socket onto the output 209 * socket, until both sockets are open and EOF has not been received. 210 */ 211 class ProxyTunnel extends Thread { 212 private final Socket sockIn; 213 private final Socket sockOut; 214 private final InputStream input; 215 private final OutputStream output; 216 ProxyTunnel(Socket sockIn, Socket sockOut)217 public ProxyTunnel(Socket sockIn, Socket sockOut) throws IOException { 218 super("ProxyTunnel"); 219 this.sockIn = sockIn; 220 this.sockOut = sockOut; 221 input = sockIn.getInputStream(); 222 output = sockOut.getOutputStream(); 223 } 224 run()225 public void run() { 226 byte[] buf = new byte[8192]; 227 int bytesRead; 228 229 try { 230 while ((bytesRead = input.read(buf)) >= 0) { 231 output.write(buf, 0, bytesRead); 232 output.flush(); 233 } 234 } catch (IOException ignored) { 235 close(); 236 } 237 } 238 close()239 public void close() { 240 try { 241 if (!sockIn.isClosed()) 242 sockIn.close(); 243 if (!sockOut.isClosed()) 244 sockOut.close(); 245 } catch (IOException ignored) { } 246 } 247 } 248 249 /** 250 * the server thread 251 */ 252 class HttpServer extends Thread implements Closeable { 253 254 private final ServerSocket serverSocket; 255 private final SSLSocketFactory sslSocketFactory; 256 private final String serverResponse = 257 "HTTP/1.1 200 OK\r\n" 258 + "Content-Type: text/plain\r\n" 259 + "Content-Length: 3\r\n" 260 + "\r\n" 261 + "Hi!"; 262 private int connectionCount = 0; 263 HttpServer()264 HttpServer() throws Exception { 265 super("HttpServer Thread"); 266 267 KeyStore ks = KeyStore.getInstance("JKS"); 268 ks.load(new FileInputStream(keystorefile), passphrase.toCharArray()); 269 KeyManagerFactory factory = KeyManagerFactory.getInstance("SunX509"); 270 factory.init(ks, passphrase.toCharArray()); 271 SSLContext ctx = SSLContext.getInstance("TLS"); 272 ctx.init(factory.getKeyManagers(), null, null); 273 274 sslSocketFactory = ctx.getSocketFactory(); 275 276 // Create the server that the test wants to connect to via the proxy 277 serverSocket = ServerSocketFactory.getDefault().createServerSocket(); 278 serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 0)); 279 } 280 getPort()281 public int getPort() { return serverSocket.getLocalPort(); } 282 283 @Override close()284 public void close() throws IOException { serverSocket.close(); } 285 286 @Override run()287 public void run() { 288 try { 289 while (connectionCount++ < 2) { 290 Socket socket = serverSocket.accept(); 291 System.out.println("Server: NEW CONNECTION " 292 + connectionCount); 293 294 SSLSocket sslSocket = (SSLSocket) sslSocketFactory 295 .createSocket(socket,null, getPort(), false); 296 sslSocket.setUseClientMode(false); 297 sslSocket.startHandshake(); 298 299 String firstLine = readHeader(sslSocket.getInputStream()); 300 if (firstLine != null && firstLine.contains("CONNECT")) { 301 System.out.println("Server: BUG! HTTP CONNECT" 302 + " encountered: " + firstLine); 303 connectInServer.set(true); 304 } 305 306 // write the success response, the request body is not read. 307 // close only output and keep input open. 308 OutputStream out = sslSocket.getOutputStream(); 309 out.write(serverResponse.getBytes("UTF-8")); 310 socket.shutdownOutput(); 311 } 312 } catch (IOException e) { 313 e.printStackTrace(); 314 } 315 } 316 } 317 318 /** 319 * read the header and return only the first line. 320 * 321 * @param inputStream the stream to read from 322 * @return the first line of the stream 323 * @throws IOException if reading failed 324 */ readHeader(InputStream inputStream)325 private static String readHeader(InputStream inputStream) 326 throws IOException { 327 String line; 328 String firstLine = null; 329 while ((line = readLine(inputStream)) != null && line.length() > 0) { 330 if (firstLine == null) { 331 firstLine = line; 332 } 333 } 334 335 return firstLine; 336 } 337 338 /** 339 * read a line from stream. 340 * 341 * @param inputStream the stream to read from 342 * @return the line 343 * @throws IOException if reading failed 344 */ readLine(InputStream inputStream)345 private static String readLine(InputStream inputStream) 346 throws IOException { 347 final StringBuilder line = new StringBuilder(); 348 int ch; 349 while ((ch = inputStream.read()) != -1) { 350 if (ch == '\r') { 351 continue; 352 } 353 354 if (ch == '\n') { 355 break; 356 } 357 358 line.append((char) ch); 359 } 360 361 return line.toString(); 362 } 363 createTestSSLSocketFactory()364 private SSLSocketFactory createTestSSLSocketFactory() { 365 366 HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { 367 @Override 368 public boolean verify(String hostname, SSLSession sslSession) { 369 // ignore the cert's CN; it's not important to this test 370 return true; 371 } 372 }); 373 374 // Set up the socket factory to use a trust manager that trusts all 375 // certs, since trust validation isn't important to this test 376 final TrustManager[] trustAllCertChains = new TrustManager[] { 377 new X509TrustManager() { 378 @Override 379 public X509Certificate[] getAcceptedIssuers() { 380 return null; 381 } 382 383 @Override 384 public void checkClientTrusted(X509Certificate[] certs, 385 String authType) { 386 } 387 388 @Override 389 public void checkServerTrusted(X509Certificate[] certs, 390 String authType) { 391 } 392 } 393 }; 394 395 final SSLContext sc; 396 try { 397 sc = SSLContext.getInstance("TLS"); 398 } catch (NoSuchAlgorithmException e) { 399 throw new RuntimeException(e); 400 } 401 402 try { 403 sc.init(null, trustAllCertChains, new java.security.SecureRandom()); 404 } catch (KeyManagementException e) { 405 throw new RuntimeException(e); 406 } 407 408 return sc.getSocketFactory(); 409 } 410 } 411