1 /*
   2  * Copyright (c) 2020, 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 /*
  25  * @test
  26  * @bug 8244205
  27  * @summary checks that a different proxy returned for
  28  *          the same host:port is taken into account
  29  * @modules java.base/sun.net.www.http
  30  *          java.net.http/jdk.internal.net.http.common
  31  *          java.net.http/jdk.internal.net.http.frame
  32  *          java.net.http/jdk.internal.net.http.hpack
  33  *          java.logging
  34  *          jdk.httpserver
  35  *          java.base/sun.net.www.http
  36  *          java.base/sun.net.www
  37  *          java.base/sun.net
  38  * @library /lib/testlibrary http2/server
  39  * @build HttpServerAdapters DigestEchoServer Http2TestServer ProxySelectorTest
  40  * @build jdk.testlibrary.SimpleSSLContext
  41  * @run testng/othervm
  42  *       -Djdk.http.auth.tunneling.disabledSchemes
  43  *       -Djdk.httpclient.HttpClient.log=headers,requests
  44  *       -Djdk.internal.httpclient.debug=true
  45  *       ProxySelectorTest
  46  */
  47 
  48 import com.sun.net.httpserver.HttpServer;
  49 import com.sun.net.httpserver.HttpsConfigurator;
  50 import com.sun.net.httpserver.HttpsServer;
  51 import jdk.testlibrary.SimpleSSLContext;
  52 import org.testng.ITestContext;
  53 import org.testng.annotations.AfterClass;
  54 import org.testng.annotations.AfterTest;
  55 import org.testng.annotations.BeforeMethod;
  56 import org.testng.annotations.BeforeTest;
  57 import org.testng.annotations.DataProvider;
  58 import org.testng.annotations.Test;
  59 
  60 import javax.net.ssl.SSLContext;
  61 import java.io.IOException;
  62 import java.io.InputStream;
  63 import java.net.InetAddress;
  64 import java.net.InetSocketAddress;
  65 import java.net.Proxy;
  66 import java.net.ProxySelector;
  67 import java.net.SocketAddress;
  68 import java.net.URI;
  69 import java.net.http.HttpClient;
  70 import java.net.http.HttpRequest;
  71 import java.net.http.HttpResponse;
  72 import java.net.http.HttpResponse.BodyHandlers;
  73 import java.util.List;
  74 import java.util.Optional;
  75 import java.util.concurrent.ConcurrentHashMap;
  76 import java.util.concurrent.ConcurrentMap;
  77 import java.util.concurrent.ExecutionException;
  78 import java.util.concurrent.Executor;
  79 import java.util.concurrent.Executors;
  80 import java.util.concurrent.atomic.AtomicLong;
  81 
  82 import static java.lang.System.err;
  83 import static java.lang.System.out;
  84 import static java.nio.charset.StandardCharsets.UTF_8;
  85 import static org.testng.Assert.assertEquals;
  86 
  87 public class ProxySelectorTest implements HttpServerAdapters {
  88 
  89     SSLContext sslContext;
  90     HttpTestServer httpTestServer;            // HTTP/1.1
  91     HttpTestServer proxyHttpTestServer;       // HTTP/1.1
  92     HttpTestServer authProxyHttpTestServer;   // HTTP/1.1
  93     HttpTestServer http2TestServer;           // HTTP/2 ( h2c )
  94     HttpTestServer httpsTestServer;           // HTTPS/1.1
  95     HttpTestServer https2TestServer;          // HTTP/2 ( h2  )
  96     DigestEchoServer.TunnelingProxy proxy;
  97     DigestEchoServer.TunnelingProxy authproxy;
  98     String httpURI;
  99     String httpsURI;
 100     String proxyHttpURI;
 101     String authProxyHttpURI;
 102     String http2URI;
 103     String https2URI;
 104     HttpClient client;
 105 
 106     final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE;
 107     static final long SLEEP_AFTER_TEST = 0; // milliseconds
 108     static final int ITERATIONS = 3;
 109     static final Executor executor = new TestExecutor(Executors.newCachedThreadPool());
 110     static final ConcurrentMap<String, Throwable> FAILURES = new ConcurrentHashMap<>();
 111     static volatile boolean tasksFailed;
 112     static final AtomicLong serverCount = new AtomicLong();
 113     static final AtomicLong clientCount = new AtomicLong();
 114     static final long start = System.nanoTime();
 115     public static String now() {
 116         long now = System.nanoTime() - start;
 117         long secs = now / 1000_000_000;
 118         long mill = (now % 1000_000_000) / 1000_000;
 119         long nan = now % 1000_000;
 120         return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan);
 121     }
 122 
 123     static class TestExecutor implements Executor {
 124         final AtomicLong tasks = new AtomicLong();
 125         Executor executor;
 126         TestExecutor(Executor executor) {
 127             this.executor = executor;
 128         }
 129 
 130         @Override
 131         public void execute(Runnable command) {
 132             long id = tasks.incrementAndGet();
 133             executor.execute(() -> {
 134                 try {
 135                     command.run();
 136                 } catch (Throwable t) {
 137                     tasksFailed = true;
 138                     out.printf(now() + "Task %s failed: %s%n", id, t);
 139                     err.printf(now() + "Task %s failed: %s%n", id, t);
 140                     FAILURES.putIfAbsent("Task " + id, t);
 141                     throw t;
 142                 }
 143             });
 144         }
 145     }
 146 
 147     protected boolean stopAfterFirstFailure() {
 148         return Boolean.getBoolean("jdk.internal.httpclient.debug");
 149     }
 150 
 151     @BeforeMethod
 152     void beforeMethod(ITestContext context) {
 153         if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) {
 154             throw new RuntimeException("some tests failed");
 155         }
 156     }
 157 
 158     @AfterClass
 159     static final void printFailedTests() {
 160         out.println("\n=========================");
 161         try {
 162             out.printf("%n%sCreated %d servers and %d clients%n",
 163                     now(), serverCount.get(), clientCount.get());
 164             if (FAILURES.isEmpty()) return;
 165             out.println("Failed tests: ");
 166             FAILURES.entrySet().forEach((e) -> {
 167                 out.printf("\t%s: %s%n", e.getKey(), e.getValue());
 168                 e.getValue().printStackTrace(out);
 169                 e.getValue().printStackTrace();
 170             });
 171             if (tasksFailed) {
 172                 out.println("WARNING: Some tasks failed");
 173             }
 174         } finally {
 175             out.println("\n=========================\n");
 176         }
 177     }
 178 
 179     /*
 180      * NOT_MODIFIED status code results from a conditional GET where
 181      * the server does not (must not) return a response body because
 182      * the condition specified in the request disallows it
 183      */
 184     static final int UNAUTHORIZED = 401;
 185     static final int PROXY_UNAUTHORIZED = 407;
 186     static final int HTTP_OK = 200;
 187     static final String MESSAGE = "Unauthorized";
 188     enum Schemes {
 189         HTTP, HTTPS
 190     }
 191     @DataProvider(name = "all")
 192     public Object[][] positive() {
 193         return new Object[][] {
 194                 { Schemes.HTTP,  HttpClient.Version.HTTP_1_1, httpURI,   true},
 195                 { Schemes.HTTP,  HttpClient.Version.HTTP_2,   http2URI,  true},
 196                 { Schemes.HTTPS, HttpClient.Version.HTTP_1_1, httpsURI,  true},
 197                 { Schemes.HTTPS, HttpClient.Version.HTTP_2,   https2URI, true},
 198                 { Schemes.HTTP,  HttpClient.Version.HTTP_1_1, httpURI,   false},
 199                 { Schemes.HTTP,  HttpClient.Version.HTTP_2,   http2URI,  false},
 200                 { Schemes.HTTPS, HttpClient.Version.HTTP_1_1, httpsURI,  false},
 201                 { Schemes.HTTPS, HttpClient.Version.HTTP_2,   https2URI, false},
 202         };
 203     }
 204 
 205     static final AtomicLong requestCounter = new AtomicLong();
 206 
 207     static final AtomicLong sleepCount = new AtomicLong();
 208 
 209     @Test(dataProvider = "all")
 210     void test(Schemes scheme, HttpClient.Version version, String uri, boolean async)
 211             throws Throwable
 212     {
 213         var name = String.format("test(%s, %s, %s)", scheme, version, async);
 214         out.printf("%n---- starting %s ----%n", name);
 215 
 216         for (int i=0; i<ITERATIONS; i++) {
 217             if (ITERATIONS > 1) out.printf("---- ITERATION %d%n",i);
 218             try {
 219                 doTest(scheme, version, uri, async);
 220                 long count = sleepCount.incrementAndGet();
 221                 System.err.println(now() + " Sleeping: " + count);
 222                 Thread.sleep(SLEEP_AFTER_TEST);
 223                 System.err.println(now() + " Waking up: " + count);
 224             } catch (Throwable x) {
 225                 FAILURES.putIfAbsent(name, x);
 226                 throw x;
 227             }
 228         }
 229     }
 230 
 231     private <T> HttpResponse<T> send(HttpClient client,
 232                                      URI uri,
 233                                      HttpResponse.BodyHandler<T> handler,
 234                                      boolean async) throws Throwable {
 235         HttpRequest.Builder requestBuilder = HttpRequest
 236                 .newBuilder(uri)
 237                 .GET();
 238 
 239         HttpRequest request = requestBuilder.build();
 240         out.println("Sending request: " + request.uri());
 241 
 242         HttpResponse<T> response = null;
 243         if (async) {
 244             response = client.send(request, handler);
 245         } else {
 246             try {
 247                 response = client.sendAsync(request, handler).get();
 248             } catch (ExecutionException ex) {
 249                 throw ex.getCause();
 250             }
 251         }
 252         return response;
 253     }
 254 
 255     private void doTest(Schemes scheme,
 256                         HttpClient.Version version,
 257                         String uriString,
 258                         boolean async) throws Throwable {
 259 
 260         URI uri1 = URI.create(uriString + "/server/ProxySelectorTest");
 261         URI uri2 = URI.create(uriString + "/proxy/noauth/ProxySelectorTest");
 262         URI uri3 = URI.create(uriString + "/proxy/auth/ProxySelectorTest");
 263 
 264         HttpResponse<String> response;
 265 
 266         // First request should go with a direct connection.
 267         // A plain server or https server should serve it, and we should get 200 OK
 268         response = send(client, uri1, BodyHandlers.ofString(), async);
 269         out.println("Got response from plain server: " + response);
 270         assertEquals(response.statusCode(), HTTP_OK);
 271         assertEquals(response.headers().firstValue("X-value"),
 272                 scheme == Schemes.HTTPS ? Optional.of("https-server") : Optional.of("plain-server"));
 273 
 274         // Second request should go through a non authenticating proxy.
 275         // For a clear connection - a proxy-server should serve it, and we should get 200 OK
 276         // For an https connection - a tunnel should be established through the non
 277         // authenticating proxy - and we should receive 200 OK from an https-server
 278         response = send(client, uri2, BodyHandlers.ofString(), async);
 279         out.println("Got response through noauth proxy: " + response);
 280         assertEquals(response.statusCode(), HTTP_OK);
 281         assertEquals(response.headers().firstValue("X-value"),
 282                 scheme == Schemes.HTTPS ? Optional.of("https-server") : Optional.of("proxy-server"));
 283 
 284         // Third request should go through an authenticating proxy.
 285         // For a clear connection - an auth-proxy-server should serve it, and we
 286         // should get 407
 287         // For an https connection - a tunnel should be established through an
 288         // authenticating proxy - and we should receive 407 directly from the
 289         // proxy - so the X-value header will be absent
 290         response = send(client, uri3, BodyHandlers.ofString(), async);
 291         out.println("Got response through auth proxy: " + response);
 292         assertEquals(response.statusCode(), PROXY_UNAUTHORIZED);
 293         assertEquals(response.headers().firstValue("X-value"),
 294                 scheme == Schemes.HTTPS ? Optional.empty() : Optional.of("auth-proxy-server"));
 295 
 296     }
 297 
 298     // -- Infrastructure
 299 
 300     @BeforeTest
 301     public void setup() throws Exception {
 302         sslContext = new SimpleSSLContext().get();
 303         if (sslContext == null)
 304             throw new AssertionError("Unexpected null sslContext");
 305 
 306         InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
 307 
 308         httpTestServer = HttpTestServer.of(HttpServer.create(sa, 0));
 309         httpTestServer.addHandler(new PlainServerHandler("plain-server"), "/http1/");
 310         httpURI = "http://" + httpTestServer.serverAuthority() + "/http1";
 311         proxyHttpTestServer = HttpTestServer.of(HttpServer.create(sa, 0));
 312         proxyHttpTestServer.addHandler(new PlainServerHandler("proxy-server"), "/http1/proxy/");
 313         proxyHttpTestServer.addHandler(new PlainServerHandler("proxy-server"), "/http2/proxy/");
 314         proxyHttpURI = "http://" + httpTestServer.serverAuthority() + "/http1";
 315         authProxyHttpTestServer = HttpTestServer.of(HttpServer.create(sa, 0));
 316         authProxyHttpTestServer.addHandler(new UnauthorizedHandler("auth-proxy-server"), "/http1/proxy/");
 317         authProxyHttpTestServer.addHandler(new UnauthorizedHandler("auth-proxy-server"), "/http2/proxy/");
 318         proxyHttpURI = "http://" + httpTestServer.serverAuthority() + "/http1";
 319 
 320         HttpsServer httpsServer = HttpsServer.create(sa, 0);
 321         httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));
 322         httpsTestServer = HttpTestServer.of(httpsServer);
 323         httpsTestServer.addHandler(new PlainServerHandler("https-server"),"/https1/");
 324         httpsURI = "https://" + httpsTestServer.serverAuthority() + "/https1";
 325 
 326         http2TestServer = HttpTestServer.of(new Http2TestServer("localhost", false, 0));
 327         http2TestServer.addHandler(new PlainServerHandler("plain-server"), "/http2/");
 328         http2URI = "http://" + http2TestServer.serverAuthority() + "/http2";
 329         https2TestServer = HttpTestServer.of(new Http2TestServer("localhost", true, sslContext));
 330         https2TestServer.addHandler(new PlainServerHandler("https-server"), "/https2/");
 331         https2URI = "https://" + https2TestServer.serverAuthority() + "/https2";
 332 
 333         proxy = DigestEchoServer.createHttpsProxyTunnel(DigestEchoServer.HttpAuthSchemeType.NONE);
 334         authproxy = DigestEchoServer.createHttpsProxyTunnel(DigestEchoServer.HttpAuthSchemeType.BASIC);
 335 
 336         client = TRACKER.track(HttpClient.newBuilder()
 337                 .proxy(new TestProxySelector())
 338                 .sslContext(sslContext)
 339                 .executor(executor)
 340                 .build());
 341         clientCount.incrementAndGet();
 342 
 343 
 344         httpTestServer.start();
 345         serverCount.incrementAndGet();
 346         proxyHttpTestServer.start();
 347         serverCount.incrementAndGet();
 348         authProxyHttpTestServer.start();
 349         serverCount.incrementAndGet();
 350         httpsTestServer.start();
 351         serverCount.incrementAndGet();
 352         http2TestServer.start();
 353         serverCount.incrementAndGet();
 354         https2TestServer.start();
 355         serverCount.incrementAndGet();
 356     }
 357 
 358     @AfterTest
 359     public void teardown() throws Exception {
 360         client = null;
 361         Thread.sleep(100);
 362         AssertionError fail = TRACKER.check(500);
 363 
 364         proxy.stop();
 365         authproxy.stop();
 366         httpTestServer.stop();
 367         proxyHttpTestServer.stop();
 368         authProxyHttpTestServer.stop();
 369         httpsTestServer.stop();
 370         http2TestServer.stop();
 371         https2TestServer.stop();
 372     }
 373 
 374     class TestProxySelector extends ProxySelector {
 375         @Override
 376         public List<Proxy> select(URI uri) {
 377             String path = uri.getPath();
 378             out.println("Selecting proxy for: " + uri);
 379             if (path.contains("/proxy/")) {
 380                 if (path.contains("/http1/") || path.contains("/http2/")) {
 381                     // Simple proxying
 382                     var p = path.contains("/auth/") ? authProxyHttpTestServer : proxyHttpTestServer;
 383                     return List.of(new Proxy(Proxy.Type.HTTP, p.getAddress()));
 384                 } else {
 385                     // Both HTTPS or HTTPS/2 require tunnelling
 386                     var p = path.contains("/auth/") ? authproxy : proxy;
 387                     return List.of(new Proxy(Proxy.Type.HTTP, p.getProxyAddress()));
 388                 }
 389             }
 390             System.out.print("NO_PROXY for " + uri);
 391             return List.of(Proxy.NO_PROXY);
 392         }
 393         @Override
 394         public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
 395             System.err.printf("Connect failed for: uri=\"%s\", sa=\"%s\", ioe=%s%n", uri, sa, ioe);
 396         }
 397     }
 398 
 399     static class PlainServerHandler implements HttpTestHandler {
 400 
 401         final String serverType;
 402         PlainServerHandler(String serverType) {
 403             this.serverType = serverType;
 404         }
 405 
 406         @Override
 407         public void handle(HttpTestExchange t) throws IOException {
 408             readAllRequestData(t); // shouldn't be any
 409             String method = t.getRequestMethod();
 410             String path = t.getRequestURI().getPath();
 411             HttpTestRequestHeaders  reqh = t.getRequestHeaders();
 412             HttpTestResponseHeaders rsph = t.getResponseHeaders();
 413 
 414             String xValue = serverType;
 415             rsph.addHeader("X-value", serverType);
 416 
 417             t.getResponseHeaders().addHeader("X-value", xValue);
 418             byte[] body = "RESPONSE".getBytes(UTF_8);
 419             t.sendResponseHeaders(HTTP_OK, body.length);
 420             try (var out = t.getResponseBody()) {
 421                 out.write(body);
 422             }
 423         }
 424     }
 425 
 426     static class UnauthorizedHandler implements HttpTestHandler {
 427 
 428         final String serverType;
 429         UnauthorizedHandler(String serverType) {
 430             this.serverType = serverType;
 431         }
 432 
 433         @Override
 434         public void handle(HttpTestExchange t) throws IOException {
 435             readAllRequestData(t); // shouldn't be any
 436             String method = t.getRequestMethod();
 437             String path = t.getRequestURI().getPath();
 438             HttpTestRequestHeaders  reqh = t.getRequestHeaders();
 439             HttpTestResponseHeaders rsph = t.getResponseHeaders();
 440 
 441             String xValue = serverType;
 442             String srv = path.contains("/proxy/") ? "proxy" : "server";
 443             String prefix = path.contains("/proxy/") ? "Proxy-" : "WWW-";
 444             int code = path.contains("/proxy/") ? PROXY_UNAUTHORIZED : UNAUTHORIZED;
 445             String resp = prefix + "Unauthorized";
 446             rsph.addHeader(prefix + "Authenticate", "Basic realm=\"earth\", charset=\"UTF-8\"");
 447 
 448             byte[] body = resp.getBytes(UTF_8);
 449             t.getResponseHeaders().addHeader("X-value", xValue);
 450             t.sendResponseHeaders(code, body.length);
 451             try (var out = t.getResponseBody()) {
 452                 out.write(body);
 453             }
 454         }
 455     }
 456 
 457     static void readAllRequestData(HttpTestExchange t) throws IOException {
 458         try (InputStream is = t.getRequestBody()) {
 459             is.readAllBytes();
 460         }
 461     }
 462 }