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