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 }