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