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 }