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 jdk.incubator.http.HttpClient; 53 import jdk.incubator.http.HttpRequest; 54 import jdk.incubator.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 jdk.incubator.httpclient 64 * @library /lib/testlibrary server 65 * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common 66 * jdk.incubator.httpclient/jdk.incubator.http.internal.frame 67 * jdk.incubator.httpclient/jdk.incubator.http.internal.hpack 68 * @build jdk.testlibrary.SimpleSSLContext ProxyTest2 69 * @run main/othervm ProxyTest2 70 * @author danielfuchs 71 */ 72 public class ProxyTest2 { 73 74 static { 75 try { 76 HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { 77 public boolean verify(String hostname, SSLSession session) { 78 return true; 79 } 80 }); 81 SSLContext.setDefault(new SimpleSSLContext().get()); 82 } catch (IOException ex) { 83 throw new ExceptionInInitializerError(ex); 84 } 85 } 86 87 static final String RESPONSE = "<html><body><p>Hello World!</body></html>"; 88 static final String PATH = "/foo/"; 89 90 static Http2TestServer createHttpsServer(ExecutorService exec) throws Exception { 91 Http2TestServer server = new Http2TestServer(true, 0, exec, SSLContext.getDefault()); 92 server.addHandler(new Http2Handler() { 93 @Override 94 public void handle(Http2TestExchange he) throws IOException { 95 he.getResponseHeaders().addHeader("encoding", "UTF-8"); 96 he.sendResponseHeaders(200, RESPONSE.length()); 97 he.getResponseBody().write(RESPONSE.getBytes(StandardCharsets.UTF_8)); 98 he.close(); 99 } 100 }, PATH); 101 102 return server; 103 } 104 105 public static void main(String[] args) 106 throws Exception 107 { 108 ExecutorService exec = Executors.newCachedThreadPool(); 109 Http2TestServer server = createHttpsServer(exec); 110 server.start(); 111 try { 112 // Http2TestServer over HTTPS does not support HTTP/1.1 113 // => only test with a HTTP/2 client 114 test(server, HttpClient.Version.HTTP_2); 115 } finally { 116 server.stop(); 117 exec.shutdown(); 118 System.out.println("Server stopped"); 119 } 120 } 121 122 public static void test(Http2TestServer server, HttpClient.Version version) 123 throws Exception 124 { 125 System.out.println("Server is: " + server.getAddress().toString()); 126 URI uri = new URI("https://localhost:" + server.getAddress().getPort() + PATH + "x"); 127 TunnelingProxy proxy = new TunnelingProxy(server); 128 proxy.start(); 129 try { 130 System.out.println("Proxy started"); 131 Proxy p = new Proxy(Proxy.Type.HTTP, 132 InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort())); 133 System.out.println("Setting up request with HttpClient for version: " 134 + version.name() + "URI=" + uri); 135 ProxySelector ps = ProxySelector.of( 136 InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort())); 137 HttpClient client = HttpClient.newBuilder() 138 .version(version) 139 .proxy(ps) 140 .build(); 141 HttpRequest request = HttpRequest.newBuilder() 142 .uri(uri) 143 .GET() 144 .build(); 145 146 System.out.println("Sending request with HttpClient"); 147 HttpResponse<String> response 148 = client.send(request, HttpResponse.BodyHandler.asString()); 149 System.out.println("Got response"); 150 String resp = response.body(); 151 System.out.println("Received: " + resp); 152 if (!RESPONSE.equals(resp)) { 153 throw new AssertionError("Unexpected response"); 154 } 155 } finally { 156 System.out.println("Stopping proxy"); 157 proxy.stop(); 158 System.out.println("Proxy stopped"); 159 } 160 } 161 162 static class TunnelingProxy { 163 final Thread accept; 164 final ServerSocket ss; 165 final boolean DEBUG = false; 166 final Http2TestServer serverImpl; 167 TunnelingProxy(Http2TestServer serverImpl) throws IOException { 168 this.serverImpl = serverImpl; 169 ss = new ServerSocket(); 170 accept = new Thread(this::accept); 171 } 172 173 void start() throws IOException { 174 ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); 175 accept.start(); 176 } 177 178 // Pipe the input stream to the output stream. 179 private synchronized Thread pipe(InputStream is, OutputStream os, char tag) { 180 return new Thread("TunnelPipe("+tag+")") { 181 @Override 182 public void run() { 183 try { 184 try { 185 int c; 186 while ((c = is.read()) != -1) { 187 os.write(c); 188 os.flush(); 189 // if DEBUG prints a + or a - for each transferred 190 // character. 191 if (DEBUG) System.out.print(tag); 192 } 193 is.close(); 194 } finally { 195 os.close(); 196 } 197 } catch (IOException ex) { 198 if (DEBUG) ex.printStackTrace(System.out); 199 } 200 } 201 }; 202 } 203 204 public InetSocketAddress getAddress() { 205 return new InetSocketAddress(ss.getInetAddress(), ss.getLocalPort()); 206 } 207 208 // This is a bit shaky. It doesn't handle continuation 209 // lines, but our client shouldn't send any. 210 // Read a line from the input stream, swallowing the final 211 // \r\n sequence. Stops at the first \n, doesn't complain 212 // if it wasn't preceded by '\r'. 213 // 214 String readLine(InputStream r) throws IOException { 215 StringBuilder b = new StringBuilder(); 216 int c; 217 while ((c = r.read()) != -1) { 218 if (c == '\n') break; 219 b.appendCodePoint(c); 220 } 221 if (b.codePointAt(b.length() -1) == '\r') { 222 b.delete(b.length() -1, b.length()); 223 } 224 return b.toString(); 225 } 226 227 public void accept() { 228 Socket clientConnection = null; 229 try { 230 while (true) { 231 System.out.println("Tunnel: Waiting for client"); 232 Socket previous = clientConnection; 233 try { 234 clientConnection = ss.accept(); 235 } catch (IOException io) { 236 if (DEBUG) io.printStackTrace(System.out); 237 break; 238 } finally { 239 // we have only 1 client at a time, so it is safe 240 // to close the previous connection here 241 if (previous != null) previous.close(); 242 } 243 System.out.println("Tunnel: Client accepted"); 244 Socket targetConnection = null; 245 InputStream ccis = clientConnection.getInputStream(); 246 OutputStream ccos = clientConnection.getOutputStream(); 247 Writer w = new OutputStreamWriter(ccos, "UTF-8"); 248 PrintWriter pw = new PrintWriter(w); 249 System.out.println("Tunnel: Reading request line"); 250 String requestLine = readLine(ccis); 251 System.out.println("Tunnel: Request status line: " + requestLine); 252 if (requestLine.startsWith("CONNECT ")) { 253 // We should probably check that the next word following 254 // CONNECT is the host:port of our HTTPS serverImpl. 255 // Some improvement for a followup! 256 257 // Read all headers until we find the empty line that 258 // signals the end of all headers. 259 while(!requestLine.equals("")) { 260 System.out.println("Tunnel: Reading header: " 261 + (requestLine = readLine(ccis))); 262 } 263 264 // Open target connection 265 targetConnection = new Socket( 266 serverImpl.getAddress().getAddress(), 267 serverImpl.getAddress().getPort()); 268 269 // Then send the 200 OK response to the client 270 System.out.println("Tunnel: Sending " 271 + "HTTP/1.1 200 OK\r\n\r\n"); 272 pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); 273 pw.flush(); 274 } else { 275 // This should not happen. 276 throw new IOException("Tunnel: Unexpected status line: " 277 + requestLine); 278 } 279 280 // Pipe the input stream of the client connection to the 281 // output stream of the target connection and conversely. 282 // Now the client and target will just talk to each other. 283 System.out.println("Tunnel: Starting tunnel pipes"); 284 Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+'); 285 Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-'); 286 t1.start(); 287 t2.start(); 288 289 // We have only 1 client... wait until it has finished before 290 // accepting a new connection request. 291 // System.out.println("Tunnel: Waiting for pipes to close"); 292 t1.join(); 293 t2.join(); 294 System.out.println("Tunnel: Done - waiting for next client"); 295 } 296 } catch (Throwable ex) { 297 try { 298 ss.close(); 299 } catch (IOException ex1) { 300 ex.addSuppressed(ex1); 301 } 302 ex.printStackTrace(System.err); 303 } 304 } 305 306 void stop() throws IOException { 307 ss.close(); 308 } 309 310 } 311 312 static class Configurator extends HttpsConfigurator { 313 public Configurator(SSLContext ctx) { 314 super(ctx); 315 } 316 317 @Override 318 public void configure (HttpsParameters params) { 319 params.setSSLParameters (getSSLContext().getSupportedSSLParameters()); 320 } 321 } 322 323 }