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.SocketAddress; 45 import java.net.URI; 46 import java.net.URISyntaxException; 47 import java.nio.charset.StandardCharsets; 48 import java.security.NoSuchAlgorithmException; 49 import java.util.List; 50 import javax.net.ssl.HostnameVerifier; 51 import javax.net.ssl.HttpsURLConnection; 52 import javax.net.ssl.SSLContext; 53 import javax.net.ssl.SSLSession; 54 import jdk.incubator.http.HttpClient; 55 import jdk.incubator.http.HttpRequest; 56 import jdk.incubator.http.HttpResponse; 57 import jdk.testlibrary.SimpleSSLContext; 58 59 /** 60 * @test 61 * @bug 8185852 8181422 62 * @summary Verifies that passing a proxy with an unresolved address does 63 * not cause java.nio.channels.UnresolvedAddressException. 64 * Verifies that downgrading from HTTP/2 to HTTP/1.1 works through 65 * an SSL Tunnel connection when the client is HTTP/2 and the server 66 * and proxy are HTTP/1.1 67 * @modules jdk.incubator.httpclient 68 * @library /lib/testlibrary/ 69 * @build jdk.testlibrary.SimpleSSLContext ProxyTest 70 * @run main/othervm ProxyTest 71 * @author danielfuchs 72 */ 73 public class ProxyTest { 74 75 static { 76 try { 77 HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { 78 public boolean verify(String hostname, SSLSession session) { 79 return true; 80 } 81 }); 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 91 static HttpServer createHttpsServer() throws IOException, NoSuchAlgorithmException { 92 HttpsServer server = com.sun.net.httpserver.HttpsServer.create(); 93 HttpContext context = server.createContext(PATH); 94 context.setHandler(new HttpHandler() { 95 @Override 96 public void handle(HttpExchange he) throws IOException { 97 he.getResponseHeaders().add("encoding", "UTF-8"); 98 he.sendResponseHeaders(200, RESPONSE.length()); 99 he.getResponseBody().write(RESPONSE.getBytes(StandardCharsets.UTF_8)); 100 he.close(); 101 } 102 }); 103 104 server.setHttpsConfigurator(new Configurator(SSLContext.getDefault())); 105 server.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0); 106 return server; 107 } 108 109 public static void main(String[] args) 110 throws IOException, 111 URISyntaxException, 112 NoSuchAlgorithmException, 113 InterruptedException 114 { 115 HttpServer server = createHttpsServer(); 116 server.start(); 117 try { 118 test(server, HttpClient.Version.HTTP_1_1); 119 test(server, HttpClient.Version.HTTP_2); 120 } finally { 121 server.stop(0); 122 System.out.println("Server stopped"); 123 } 124 } 125 126 /** 127 * A Proxy Selector that wraps a ProxySelector.of(), and counts the number 128 * of times its select method has been invoked. This can be used to ensure 129 * that the Proxy Selector is invoked only once per HttpClient.sendXXX 130 * invocation. 131 */ 132 static class CountingProxySelector extends ProxySelector { 133 private final ProxySelector proxySelector; 134 private volatile int count; // 0 135 private CountingProxySelector(InetSocketAddress proxyAddress) { 136 proxySelector = ProxySelector.of(proxyAddress); 137 } 138 139 public static CountingProxySelector of(InetSocketAddress proxyAddress) { 140 return new CountingProxySelector(proxyAddress); 141 } 142 143 int count() { return count; } 144 145 @Override 146 public List<Proxy> select(URI uri) { 147 count++; 148 return proxySelector.select(uri); 149 } 150 151 @Override 152 public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { 153 proxySelector.connectFailed(uri, sa, ioe); 154 } 155 } 156 157 public static void test(HttpServer server, HttpClient.Version version) 158 throws IOException, 159 URISyntaxException, 160 NoSuchAlgorithmException, 161 InterruptedException 162 { 163 System.out.println("Server is: " + server.getAddress().toString()); 164 System.out.println("Verifying communication with server"); 165 URI uri = new URI("https:/" + server.getAddress().toString() + PATH + "x"); 166 try (InputStream is = uri.toURL().openConnection().getInputStream()) { 167 String resp = new String(is.readAllBytes(), StandardCharsets.UTF_8); 168 System.out.println(resp); 169 if (!RESPONSE.equals(resp)) { 170 throw new AssertionError("Unexpected response from server"); 171 } 172 } 173 System.out.println("Communication with server OK"); 174 175 TunnelingProxy proxy = new TunnelingProxy(server); 176 proxy.start(); 177 try { 178 System.out.println("Proxy started"); 179 Proxy p = new Proxy(Proxy.Type.HTTP, 180 InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort())); 181 System.out.println("Verifying communication with proxy"); 182 HttpURLConnection conn = (HttpURLConnection)uri.toURL().openConnection(p); 183 try (InputStream is = conn.getInputStream()) { 184 String resp = new String(is.readAllBytes(), StandardCharsets.UTF_8); 185 System.out.println(resp); 186 if (!RESPONSE.equals(resp)) { 187 throw new AssertionError("Unexpected response from proxy"); 188 } 189 } 190 System.out.println("Communication with proxy OK"); 191 System.out.println("\nReal test begins here."); 192 System.out.println("Setting up request with HttpClient for version: " 193 + version.name()); 194 CountingProxySelector ps = CountingProxySelector.of( 195 InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort())); 196 HttpClient client = HttpClient.newBuilder() 197 .version(version) 198 .proxy(ps) 199 .build(); 200 HttpRequest request = HttpRequest.newBuilder() 201 .uri(uri) 202 .GET() 203 .build(); 204 205 System.out.println("Sending request with HttpClient"); 206 HttpResponse<String> response 207 = client.send(request, HttpResponse.BodyHandler.asString()); 208 System.out.println("Got response"); 209 String resp = response.body(); 210 System.out.println("Received: " + resp); 211 if (!RESPONSE.equals(resp)) { 212 throw new AssertionError("Unexpected response"); 213 } 214 if (ps.count() > 1) { 215 throw new AssertionError("CountingProxySelector. Expected 1, got " + ps.count()); 216 } 217 } finally { 218 System.out.println("Stopping proxy"); 219 proxy.stop(); 220 System.out.println("Proxy stopped"); 221 } 222 } 223 224 static class TunnelingProxy { 225 final Thread accept; 226 final ServerSocket ss; 227 final boolean DEBUG = false; 228 final HttpServer serverImpl; 229 TunnelingProxy(HttpServer serverImpl) throws IOException { 230 this.serverImpl = serverImpl; 231 ss = new ServerSocket(); 232 accept = new Thread(this::accept); 233 } 234 235 void start() throws IOException { 236 ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); 237 accept.start(); 238 } 239 240 // Pipe the input stream to the output stream. 241 private synchronized Thread pipe(InputStream is, OutputStream os, char tag) { 242 return new Thread("TunnelPipe("+tag+")") { 243 @Override 244 public void run() { 245 try { 246 try { 247 int c; 248 while ((c = is.read()) != -1) { 249 os.write(c); 250 os.flush(); 251 // if DEBUG prints a + or a - for each transferred 252 // character. 253 if (DEBUG) System.out.print(tag); 254 } 255 is.close(); 256 } finally { 257 os.close(); 258 } 259 } catch (IOException ex) { 260 if (DEBUG) ex.printStackTrace(System.out); 261 } 262 } 263 }; 264 } 265 266 public InetSocketAddress getAddress() { 267 return new InetSocketAddress(ss.getInetAddress(), ss.getLocalPort()); 268 } 269 270 // This is a bit shaky. It doesn't handle continuation 271 // lines, but our client shouldn't send any. 272 // Read a line from the input stream, swallowing the final 273 // \r\n sequence. Stops at the first \n, doesn't complain 274 // if it wasn't preceded by '\r'. 275 // 276 String readLine(InputStream r) throws IOException { 277 StringBuilder b = new StringBuilder(); 278 int c; 279 while ((c = r.read()) != -1) { 280 if (c == '\n') break; 281 b.appendCodePoint(c); 282 } 283 if (b.codePointAt(b.length() -1) == '\r') { 284 b.delete(b.length() -1, b.length()); 285 } 286 return b.toString(); 287 } 288 289 public void accept() { 290 Socket clientConnection = null; 291 try { 292 while (true) { 293 System.out.println("Tunnel: Waiting for client"); 294 Socket previous = clientConnection; 295 try { 296 clientConnection = ss.accept(); 297 } catch (IOException io) { 298 if (DEBUG) io.printStackTrace(System.out); 299 break; 300 } finally { 301 // we have only 1 client at a time, so it is safe 302 // to close the previous connection here 303 if (previous != null) previous.close(); 304 } 305 System.out.println("Tunnel: Client accepted"); 306 Socket targetConnection = null; 307 InputStream ccis = clientConnection.getInputStream(); 308 OutputStream ccos = clientConnection.getOutputStream(); 309 Writer w = new OutputStreamWriter(ccos, "UTF-8"); 310 PrintWriter pw = new PrintWriter(w); 311 System.out.println("Tunnel: Reading request line"); 312 String requestLine = readLine(ccis); 313 System.out.println("Tunnel: Request status line: " + requestLine); 314 if (requestLine.startsWith("CONNECT ")) { 315 // We should probably check that the next word following 316 // CONNECT is the host:port of our HTTPS serverImpl. 317 // Some improvement for a followup! 318 319 // Read all headers until we find the empty line that 320 // signals the end of all headers. 321 while(!requestLine.equals("")) { 322 System.out.println("Tunnel: Reading header: " 323 + (requestLine = readLine(ccis))); 324 } 325 326 // Open target connection 327 targetConnection = new Socket( 328 serverImpl.getAddress().getAddress(), 329 serverImpl.getAddress().getPort()); 330 331 // Then send the 200 OK response to the client 332 System.out.println("Tunnel: Sending " 333 + "HTTP/1.1 200 OK\r\n\r\n"); 334 pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); 335 pw.flush(); 336 } else { 337 // This should not happen. 338 throw new IOException("Tunnel: Unexpected status line: " 339 + requestLine); 340 } 341 342 // Pipe the input stream of the client connection to the 343 // output stream of the target connection and conversely. 344 // Now the client and target will just talk to each other. 345 System.out.println("Tunnel: Starting tunnel pipes"); 346 Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+'); 347 Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-'); 348 t1.start(); 349 t2.start(); 350 351 // We have only 1 client... wait until it has finished before 352 // accepting a new connection request. 353 // System.out.println("Tunnel: Waiting for pipes to close"); 354 // t1.join(); 355 // t2.join(); 356 System.out.println("Tunnel: Done - waiting for next client"); 357 } 358 } catch (Throwable ex) { 359 try { 360 ss.close(); 361 } catch (IOException ex1) { 362 ex.addSuppressed(ex1); 363 } 364 ex.printStackTrace(System.err); 365 } 366 } 367 368 void stop() throws IOException { 369 ss.close(); 370 } 371 372 } 373 374 static class Configurator extends HttpsConfigurator { 375 public Configurator(SSLContext ctx) { 376 super(ctx); 377 } 378 379 @Override 380 public void configure (HttpsParameters params) { 381 params.setSSLParameters (getSSLContext().getSupportedSSLParameters()); 382 } 383 } 384 385 }