1 /*
2  * Copyright (c) 2017, 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 com.sun.net.httpserver.HttpContext;
25 import com.sun.net.httpserver.HttpExchange;
26 import com.sun.net.httpserver.HttpHandler;
27 import com.sun.net.httpserver.HttpServer;
28 import com.sun.net.httpserver.HttpsConfigurator;
29 import com.sun.net.httpserver.HttpsParameters;
30 import com.sun.net.httpserver.HttpsServer;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.io.OutputStream;
34 import java.io.OutputStreamWriter;
35 import java.io.PrintWriter;
36 import java.io.Writer;
37 import java.net.HttpURLConnection;
38 import java.net.InetAddress;
39 import java.net.InetSocketAddress;
40 import java.net.Proxy;
41 import java.net.ProxySelector;
42 import java.net.ServerSocket;
43 import java.net.Socket;
44 import java.net.URI;
45 import java.net.URISyntaxException;
46 import java.nio.charset.StandardCharsets;
47 import java.security.NoSuchAlgorithmException;
48 import javax.net.ssl.HostnameVerifier;
49 import javax.net.ssl.HttpsURLConnection;
50 import javax.net.ssl.SSLContext;
51 import javax.net.ssl.SSLSession;
52 import java.net.http.HttpClient;
53 import java.net.http.HttpRequest;
54 import java.net.http.HttpResponse;
55 import jdk.testlibrary.SimpleSSLContext;
56 import java.util.concurrent.*;
57 
58 /**
59  * @test
60  * @bug 8181422
61  * @summary  Verifies that you can access an HTTP/2 server over HTTPS by
62  *           tunnelling through an HTTP/1.1 proxy.
63  * @modules java.net.http
64  * @library /lib/testlibrary server
65  * @modules java.base/sun.net.www.http
66  *          java.net.http/jdk.internal.net.http.common
67  *          java.net.http/jdk.internal.net.http.frame
68  *          java.net.http/jdk.internal.net.http.hpack
69  * @build jdk.testlibrary.SimpleSSLContext ProxyTest2
70  * @run main/othervm ProxyTest2
71  * @author danielfuchs
72  */
73 public class ProxyTest2 {
74 
75     static {
76         try {
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { public boolean verify(String hostname, SSLSession session) { return true; } })77             HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
78                     public boolean verify(String hostname, SSLSession session) {
79                         return true;
80                     }
81                 });
SSLContext.setDefault(new SimpleSSLContext().get())82             SSLContext.setDefault(new SimpleSSLContext().get());
83         } catch (IOException ex) {
84             throw new ExceptionInInitializerError(ex);
85         }
86     }
87 
88     static final String RESPONSE = "<html><body><p>Hello World!</body></html>";
89     static final String PATH = "/foo/";
90 
createHttpsServer(ExecutorService exec)91     static Http2TestServer createHttpsServer(ExecutorService exec) throws Exception {
92         Http2TestServer server = new Http2TestServer(true, 0, exec, SSLContext.getDefault());
93         server.addHandler(new Http2Handler() {
94             @Override
95             public void handle(Http2TestExchange he) throws IOException {
96                 he.getResponseHeaders().addHeader("encoding", "UTF-8");
97                 he.sendResponseHeaders(200, RESPONSE.length());
98                 he.getResponseBody().write(RESPONSE.getBytes(StandardCharsets.UTF_8));
99                 he.close();
100             }
101         }, PATH);
102 
103         return server;
104     }
105 
main(String[] args)106     public static void main(String[] args)
107             throws Exception
108     {
109         ExecutorService exec = Executors.newCachedThreadPool();
110         Http2TestServer server = createHttpsServer(exec);
111         server.start();
112         try {
113             // Http2TestServer over HTTPS does not support HTTP/1.1
114             // => only test with a HTTP/2 client
115             test(server, HttpClient.Version.HTTP_2);
116         } finally {
117             server.stop();
118             exec.shutdown();
119             System.out.println("Server stopped");
120         }
121     }
122 
test(Http2TestServer server, HttpClient.Version version)123     public static void test(Http2TestServer server, HttpClient.Version version)
124             throws Exception
125     {
126         System.out.println("Server is: " + server.getAddress().toString());
127         URI uri = new URI("https://localhost:" + server.getAddress().getPort() + PATH + "x");
128         TunnelingProxy proxy = new TunnelingProxy(server);
129         proxy.start();
130         try {
131             System.out.println("Proxy started");
132             Proxy p = new Proxy(Proxy.Type.HTTP,
133                     InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort()));
134             System.out.println("Setting up request with HttpClient for version: "
135                     + version.name() + "URI=" + uri);
136             ProxySelector ps = ProxySelector.of(
137                     InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort()));
138             HttpClient client = HttpClient.newBuilder()
139                 .version(version)
140                 .proxy(ps)
141                 .build();
142             HttpRequest request = HttpRequest.newBuilder()
143                 .uri(uri)
144                 .GET()
145                 .build();
146 
147             System.out.println("Sending request with HttpClient");
148             HttpResponse<String> response
149                 = client.send(request, HttpResponse.BodyHandlers.ofString());
150             System.out.println("Got response");
151             String resp = response.body();
152             System.out.println("Received: " + resp);
153             if (!RESPONSE.equals(resp)) {
154                 throw new AssertionError("Unexpected response");
155             }
156         } finally {
157             System.out.println("Stopping proxy");
158             proxy.stop();
159             System.out.println("Proxy stopped");
160         }
161     }
162 
163     static class TunnelingProxy {
164         final Thread accept;
165         final ServerSocket ss;
166         final boolean DEBUG = false;
167         final Http2TestServer serverImpl;
168         final CopyOnWriteArrayList<CompletableFuture<Void>> connectionCFs
169                 = new CopyOnWriteArrayList<>();
170         private volatile boolean stopped;
TunnelingProxy(Http2TestServer serverImpl)171         TunnelingProxy(Http2TestServer serverImpl) throws IOException {
172             this.serverImpl = serverImpl;
173             ss = new ServerSocket();
174             accept = new Thread(this::accept);
175             accept.setDaemon(true);
176         }
177 
start()178         void start() throws IOException {
179             ss.setReuseAddress(false);
180             ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
181             accept.start();
182         }
183 
184         // Pipe the input stream to the output stream.
pipe(InputStream is, OutputStream os, char tag, CompletableFuture<Void> end)185         private synchronized Thread pipe(InputStream is, OutputStream os,
186                                          char tag, CompletableFuture<Void> end) {
187             return new Thread("TunnelPipe("+tag+")") {
188                 @Override
189                 public void run() {
190                     try {
191                         try {
192                             int c;
193                             while ((c = is.read()) != -1) {
194                                 os.write(c);
195                                 os.flush();
196                                 // if DEBUG prints a + or a - for each transferred
197                                 // character.
198                                 if (DEBUG) System.out.print(tag);
199                             }
200                             is.close();
201                         } finally {
202                             os.close();
203                         }
204                     } catch (IOException ex) {
205                         if (DEBUG) ex.printStackTrace(System.out);
206                     } finally {
207                         end.complete(null);
208                     }
209                 }
210             };
211         }
212 
213         public InetSocketAddress getAddress() {
214             return new InetSocketAddress( InetAddress.getLoopbackAddress(), ss.getLocalPort());
215         }
216 
217         // This is a bit shaky. It doesn't handle continuation
218         // lines, but our client shouldn't send any.
219         // Read a line from the input stream, swallowing the final
220         // \r\n sequence. Stops at the first \n, doesn't complain
221         // if it wasn't preceded by '\r'.
222         //
223         String readLine(InputStream r) throws IOException {
224             StringBuilder b = new StringBuilder();
225             int c;
226             while ((c = r.read()) != -1) {
227                 if (c == '\n') break;
228                 b.appendCodePoint(c);
229             }
230             if (b.codePointAt(b.length() -1) == '\r') {
231                 b.delete(b.length() -1, b.length());
232             }
233             return b.toString();
234         }
235 
236         public void accept() {
237             Socket clientConnection = null;
238             try {
239                 while (!stopped) {
240                     System.out.println("Tunnel: Waiting for client");
241                     Socket toClose;
242                     try {
243                         toClose = clientConnection = ss.accept();
244                     } catch (IOException io) {
245                         if (DEBUG) io.printStackTrace(System.out);
246                         break;
247                     }
248                     System.out.println("Tunnel: Client accepted");
249                     Socket targetConnection = null;
250                     InputStream  ccis = clientConnection.getInputStream();
251                     OutputStream ccos = clientConnection.getOutputStream();
252                     Writer w = new OutputStreamWriter(ccos, "UTF-8");
253                     PrintWriter pw = new PrintWriter(w);
254                     System.out.println("Tunnel: Reading request line");
255                     String requestLine = readLine(ccis);
256                     System.out.println("Tunnel: Request status line: " + requestLine);
257                     if (requestLine.startsWith("CONNECT ")) {
258                         // We should probably check that the next word following
259                         // CONNECT is the host:port of our HTTPS serverImpl.
260                         // Some improvement for a followup!
261 
262                         // Read all headers until we find the empty line that
263                         // signals the end of all headers.
264                         while(!requestLine.equals("")) {
265                             System.out.println("Tunnel: Reading header: "
266                                     + (requestLine = readLine(ccis)));
267                         }
268 
269                         // Open target connection
270                         targetConnection = new Socket(
271                                 InetAddress.getLoopbackAddress(),
272                                 serverImpl.getAddress().getPort());
273 
274                         // Then send the 200 OK response to the client
275                         System.out.println("Tunnel: Sending "
276                                 + "HTTP/1.1 200 OK\r\n\r\n");
277                         pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
278                         pw.flush();
279                     } else {
280                         // This should not happen. If it does then just print an
281                         // error - both on out and err, and close the accepted
282                         // socket
283                         System.out.println("WARNING: Tunnel: Unexpected status line: "
284                                 + requestLine + " received by "
285                                 + ss.getLocalSocketAddress()
286                                 + " from "
287                                 + toClose.getRemoteSocketAddress()
288                                 + " - closing accepted socket");
289                         // Print on err
290                         System.err.println("WARNING: Tunnel: Unexpected status line: "
291                                 + requestLine + " received by "
292                                 + ss.getLocalSocketAddress()
293                                 + " from "
294                                 + toClose.getRemoteSocketAddress());
295                         // close accepted socket.
296                         toClose.close();
297                         System.err.println("Tunnel: accepted socket closed.");
298                         continue;
299                     }
300 
301                     // Pipe the input stream of the client connection to the
302                     // output stream of the target connection and conversely.
303                     // Now the client and target will just talk to each other.
304                     System.out.println("Tunnel: Starting tunnel pipes");
305                     CompletableFuture<Void> end, end1, end2;
306                     Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+',
307                             end1 = new CompletableFuture<>());
308                     Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-',
309                             end2 = new CompletableFuture<>());
310                     end = CompletableFuture.allOf(end1, end2);
311                     end.whenComplete(
312                             (r,t) -> {
313                                 try { toClose.close(); } catch (IOException x) { }
314                                 finally {connectionCFs.remove(end);}
315                             });
316                     connectionCFs.add(end);
317                     t1.start();
318                     t2.start();
319                 }
320             } catch (Throwable ex) {
321                 try {
322                     ss.close();
323                 } catch (IOException ex1) {
324                     ex.addSuppressed(ex1);
325                 }
326                 ex.printStackTrace(System.err);
327             } finally {
328                 System.out.println("Tunnel: exiting (stopped=" + stopped + ")");
329                 connectionCFs.forEach(cf -> cf.complete(null));
330             }
331         }
332 
333         public void stop() throws IOException {
334             stopped = true;
335             ss.close();
336         }
337 
338     }
339 
340     static class Configurator extends HttpsConfigurator {
341         public Configurator(SSLContext ctx) {
342             super(ctx);
343         }
344 
345         @Override
346         public void configure (HttpsParameters params) {
347             params.setSSLParameters (getSSLContext().getSupportedSSLParameters());
348         }
349     }
350 
351 }
352