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