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 }