http-client-branch: Add whitebox tests for AuthenticationFilter. http-client-branch
authordfuchs
Mon, 12 Feb 2018 17:32:52 +0000
branchhttp-client-branch
changeset 56103 d5f70938e399
parent 56102 34f5fa24fdfb
child 56104 3420c1bdd254
http-client-branch: Add whitebox tests for AuthenticationFilter.
src/java.net.http/share/classes/jdk/internal/net/http/AuthenticationFilter.java
src/java.net.http/share/classes/jdk/internal/net/http/hpack/HeaderTable.java
test/jdk/java/net/httpclient/whitebox/AuthenticationFilterTestDriver.java
test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/AuthenticationFilterTest.java
--- a/src/java.net.http/share/classes/jdk/internal/net/http/AuthenticationFilter.java	Sat Feb 10 08:39:29 2018 +0000
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/AuthenticationFilter.java	Mon Feb 12 17:32:52 2018 +0000
@@ -82,12 +82,28 @@
         String realm = parser.findValue("realm");
         java.net.Authenticator.RequestorType rtype = proxy ? PROXY : SERVER;
         URL url = toURL(uri, req.method(), proxy);
+        String host;
+        int port;
+        String protocol;
+        InetSocketAddress proxyAddress;
+        if (proxy && (proxyAddress = req.proxy()) != null) {
+            // request sent to server through proxy
+            proxyAddress = req.proxy();
+            host = proxyAddress.getHostString();
+            port = proxyAddress.getPort();
+            protocol = "http"; // we don't support https connection to proxy
+        } else {
+            // direct connection to server or proxy
+            host = uri.getHost();
+            port = uri.getPort();
+            protocol = uri.getScheme();
+        }
 
         // needs to be instance method in Authenticator
-        return auth.requestPasswordAuthenticationInstance(uri.getHost(),
+        return auth.requestPasswordAuthenticationInstance(host,
                                                           null,
-                                                          uri.getPort(),
-                                                          uri.getScheme(),
+                                                          port,
+                                                          protocol,
                                                           realm,
                                                           authscheme,
                                                           url,
@@ -119,7 +135,7 @@
                            null,
                            proxy.getHostString(),
                            proxy.getPort(),
-                           null,
+                           "/",
                            null,
                            null);
         } catch (URISyntaxException e) {
@@ -235,7 +251,10 @@
             }
             if (exchange.proxyauth != null && !exchange.proxyauth.fromcache) {
                 AuthInfo au = exchange.proxyauth;
-                cache.store(au.scheme, req.uri(), false, au.credentials);
+                URI proxyURI = getProxyURI(req);
+                if (proxyURI != null) {
+                    cache.store(au.scheme, proxyURI, true, au.credentials);
+                }
             }
             return null;
         }
@@ -334,9 +353,11 @@
     // Note: Make sure that Cache and CacheEntry do not keep any strong
     //       reference to the HttpClient: it would prevent the client being
     //       GC'ed when no longer referenced.
-    static class Cache {
+    static final class Cache {
         final LinkedList<CacheEntry> entries = new LinkedList<>();
 
+        Cache() {}
+
         synchronized CacheEntry get(URI uri, boolean proxy) {
             for (CacheEntry entry : entries) {
                 if (entry.equalsKey(uri, proxy)) {
@@ -367,7 +388,26 @@
         }
     }
 
-    static class CacheEntry {
+    static URI normalize(URI uri, boolean isPrimaryKey) {
+        String path = uri.getPath();
+        if (path == null || path.isEmpty()) {
+            // make sure the URI has a path, ignore query and fragment
+            try {
+                return new URI(uri.getScheme(), uri.getAuthority(), "/", null, null);
+            } catch (URISyntaxException e) {
+                throw new InternalError(e);
+            }
+        } else if (isPrimaryKey || !"/".equals(path)) {
+            // remove extraneous components and normalize path
+            return uri.resolve(".");
+        } else {
+            // path == "/" and the URI is not used to store
+            // the primary key in the cache: nothing to do.
+            return uri;
+        }
+    }
+
+    static final class CacheEntry {
         final String root;
         final String scheme;
         final boolean proxy;
@@ -378,7 +418,7 @@
                    boolean proxy,
                    PasswordAuthentication value) {
             this.scheme = authscheme;
-            this.root = uri.resolve(".").toString(); // remove extraneous components
+            this.root = normalize(uri, true).toString(); // remove extraneous components
             this.proxy = proxy;
             this.value = value;
         }
@@ -391,7 +431,7 @@
             if (this.proxy != proxy) {
                 return false;
             }
-            String other = uri.toString();
+            String other = String.valueOf(normalize(uri, false));
             return other.startsWith(root);
         }
     }
--- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/HeaderTable.java	Sat Feb 10 08:39:29 2018 +0000
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/HeaderTable.java	Mon Feb 12 17:32:52 2018 +0000
@@ -304,7 +304,9 @@
         StringBuilder b = new StringBuilder();
         for (int i = 1, size = dynamicTable.size(); i <= size; i++) {
             HeaderField e = dynamicTable.get(i);
-            b.append(format("[%3d] (s = %3d) %s: %s%n", i,
+            // Use \n instead of %n as this output is used in
+            // tests with string comparison.
+            b.append(format("[%3d] (s = %3d) %s: %s\n", i,
                     sizeOf(e), e.name, e.value));
         }
         b.append(format("      Table size:%4s", this.size));
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/AuthenticationFilterTestDriver.java	Mon Feb 12 17:32:52 2018 +0000
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @modules java.net.http/jdk.internal.net.http
+ * @run testng java.net.http/jdk.internal.net.http.AuthenticationFilterTest
+ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/AuthenticationFilterTest.java	Mon Feb 12 17:32:52 2018 +0000
@@ -0,0 +1,568 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.internal.net.http;
+
+import jdk.internal.net.http.common.HttpHeadersImpl;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import org.testng.annotations.AfterClass;
+
+import java.lang.ref.Reference;
+import java.net.Authenticator;
+import java.net.InetSocketAddress;
+import java.net.PasswordAuthentication;
+import java.net.ProxySelector;
+import java.net.URI;
+import java.net.URL;
+import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpResponse;
+import java.security.AccessController;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicLong;
+import java.net.http.HttpClient.Version;
+
+import static java.lang.String.format;
+import static java.lang.System.out;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.util.stream.Collectors.joining;
+import static java.net.http.HttpClient.Version.HTTP_1_1;
+import static java.net.http.HttpClient.Version.HTTP_2;
+import static java.net.http.HttpClient.Builder.NO_PROXY;
+import static org.testng.Assert.*;
+
+public class AuthenticationFilterTest {
+
+    @DataProvider(name = "uris")
+    public Object[][] responses() {
+        return new Object[][] {
+                { "http://foo.com", HTTP_1_1, null },
+                { "http://foo.com", HTTP_2, null },
+                { "http://foo.com#blah", HTTP_1_1, null },
+                { "http://foo.com#blah", HTTP_2, null },
+                { "http://foo.com/x/y/z", HTTP_1_1, null },
+                { "http://foo.com/x/y/z", HTTP_2, null },
+                { "http://foo.com/x/y/z#blah", HTTP_1_1, null },
+                { "http://foo.com/x/y/z#blah", HTTP_2, null },
+                { "http://foo.com:80", HTTP_1_1, null },
+                { "http://foo.com:80", HTTP_2, null },
+                { "http://foo.com:80#blah", HTTP_1_1, null },
+                { "http://foo.com:80#blah", HTTP_2, null },
+                { "http://foo.com", HTTP_1_1, "127.0.0.1:8080" },
+                { "http://foo.com", HTTP_2, "127.0.0.1:8080" },
+                { "http://foo.com#blah", HTTP_1_1, "127.0.0.1:8080" },
+                { "http://foo.com#blah", HTTP_2, "127.0.0.1:8080" },
+                { "http://foo.com:8080", HTTP_1_1, "127.0.0.1:8080" },
+                { "http://foo.com:8080", HTTP_2, "127.0.0.1:8080" },
+                { "http://foo.com:8080#blah", HTTP_1_1, "127.0.0.1:8080" },
+                { "http://foo.com:8080#blah", HTTP_2, "127.0.0.1:8080" },
+                { "https://foo.com", HTTP_1_1, null },
+                { "https://foo.com", HTTP_2, null },
+                { "https://foo.com#blah", HTTP_1_1, null },
+                { "https://foo.com#blah", HTTP_2, null },
+                { "https://foo.com:443", HTTP_1_1, null },
+                { "https://foo.com:443", HTTP_2, null },
+                { "https://foo.com:443#blah", HTTP_1_1, null },
+                { "https://foo.com:443#blah", HTTP_2, null },
+                { "https://foo.com", HTTP_1_1, "127.0.0.1:8080" },
+                { "https://foo.com", HTTP_2, "127.0.0.1:8080" },
+                { "https://foo.com#blah", HTTP_1_1, "127.0.0.1:8080" },
+                { "https://foo.com#blah", HTTP_2, "127.0.0.1:8080" },
+                { "https://foo.com:8080", HTTP_1_1, "127.0.0.1:8080" },
+                { "https://foo.com:8080", HTTP_2, "127.0.0.1:8080" },
+                { "https://foo.com:8080#blah", HTTP_1_1, "127.0.0.1:8080" },
+                { "https://foo.com:8080#blah", HTTP_2, "127.0.0.1:8080" },
+                { "http://foo.com:80/x/y/z", HTTP_1_1, null },
+                { "http://foo.com:80/x/y/z", HTTP_2, null },
+                { "http://foo.com:80/x/y/z#blah", HTTP_1_1, null },
+                { "http://foo.com:80/x/y/z#blah", HTTP_2, null },
+                { "http://foo.com/x/y/z", HTTP_1_1, "127.0.0.1:8080" },
+                { "http://foo.com/x/y/z", HTTP_2, "127.0.0.1:8080" },
+                { "http://foo.com/x/y/z#blah", HTTP_1_1, "127.0.0.1:8080" },
+                { "http://foo.com/x/y/z#blah", HTTP_2, "127.0.0.1:8080" },
+                { "http://foo.com:8080/x/y/z", HTTP_1_1, "127.0.0.1:8080" },
+                { "http://foo.com:8080/x/y/z", HTTP_2, "127.0.0.1:8080" },
+                { "http://foo.com:8080/x/y/z#blah", HTTP_1_1, "127.0.0.1:8080" },
+                { "http://foo.com:8080/x/y/z#blah", HTTP_2, "127.0.0.1:8080" },
+                { "https://foo.com/x/y/z", HTTP_1_1, null },
+                { "https://foo.com/x/y/z", HTTP_2, null },
+                { "https://foo.com/x/y/z#blah", HTTP_1_1, null },
+                { "https://foo.com/x/y/z#blah", HTTP_2, null },
+                { "https://foo.com:443/x/y/z", HTTP_1_1, null },
+                { "https://foo.com:443/x/y/z", HTTP_2, null },
+                { "https://foo.com:443/x/y/z#blah", HTTP_1_1, null },
+                { "https://foo.com:443/x/y/z#blah", HTTP_2, null },
+                { "https://foo.com/x/y/z", HTTP_1_1, "127.0.0.1:8080" },
+                { "https://foo.com/x/y/z", HTTP_2, "127.0.0.1:8080" },
+                { "https://foo.com/x/y/z#blah", HTTP_1_1, "127.0.0.1:8080" },
+                { "https://foo.com/x/y/z#blah", HTTP_2, "127.0.0.1:8080" },
+                { "https://foo.com:8080/x/y/z", HTTP_1_1, "127.0.0.1:8080" },
+                { "https://foo.com:8080/x/y/z", HTTP_2, "127.0.0.1:8080" },
+                { "https://foo.com:8080/x/y/z#blah", HTTP_1_1, "127.0.0.1:8080" },
+                { "https://foo.com:8080/x/y/z#blah", HTTP_2, "127.0.0.1:8080" },
+        };
+    }
+
+    static final ConcurrentMap<String,Throwable> FAILED = new ConcurrentHashMap<>();
+
+    static boolean isNullOrEmpty(String s) {
+        return s == null || s.isEmpty();
+    }
+
+    @Test(dataProvider = "uris")
+    public void testAuthentication(String uri, Version v, String proxy) throws Exception {
+        String test = format("testAuthentication: {\"%s\", %s, \"%s\"}", uri, v, proxy);
+        try {
+            doTestAuthentication(uri, v, proxy);
+        } catch(Exception | Error x) {
+            FAILED.putIfAbsent(test, x);
+            throw x;
+        }
+    }
+
+    @AfterClass
+    public void printDiagnostic() {
+        if (FAILED.isEmpty()) {
+            out.println("All tests passed");
+            return;
+        }
+        // make sure failures don't disappear in the overflow
+        out.println("Failed tests: ");
+        FAILED.keySet().forEach(s ->
+                out.println("\t " + s.substring(s.indexOf(':')+1) + ","));
+        out.println();
+        FAILED.entrySet().forEach(e -> {
+                System.err.println("\n" + e.getKey()
+                        + " FAILED: " + e.getValue());
+                e.getValue().printStackTrace();
+        });
+    }
+
+    private void doTestAuthentication(String uri, Version v, String proxy) throws Exception {
+        int colon = proxy == null ? -1 : proxy.lastIndexOf(":");
+        ProxySelector ps = proxy == null ? NO_PROXY
+                : ProxySelector.of(InetSocketAddress.createUnresolved(
+                        proxy.substring(0, colon),
+                        Integer.parseInt(proxy.substring(colon+1))));
+        int unauthorized = proxy == null ? 401 : 407;
+
+        TestAuthenticator authenticator = new TestAuthenticator();
+
+        // Creates a HttpClientImpl
+        HttpClientBuilderImpl clientBuilder = new HttpClientBuilderImpl()
+                .authenticator(authenticator).proxy(ps);
+        HttpClientFacade facade = HttpClientImpl.create(clientBuilder);
+        HttpClientImpl client = facade.impl;
+        AuthenticationFilter filter = new AuthenticationFilter();
+
+        assertEquals(authenticator.COUNTER.get(), 0);
+
+        // Creates the first HttpRequestImpl, and call filter.request() with
+        // it. The expectation is that the filter will not add any credentials,
+        // because the cache is empty and we don't know which auth schemes the
+        // server supports yet.
+        URI reqURI = URI.create(uri);
+        HttpRequestBuilderImpl reqBuilder =
+                new HttpRequestBuilderImpl(reqURI);
+        HttpRequestImpl origReq = new HttpRequestImpl(reqBuilder);
+        HttpRequestImpl req = new HttpRequestImpl(origReq, ps, AccessController.getContext());
+        MultiExchange<?> multi = new MultiExchange<Void>(origReq, req, client,
+                HttpResponse.BodyHandler.replace(null),
+                null, AccessController.getContext());
+        Exchange<?> exchange = new Exchange<>(req, multi);
+        out.println("\nSimulating unauthenticated request to " + uri);
+        filter.request(req, multi);
+        assertFalse(req.getSystemHeaders().firstValue(authorization(true)).isPresent());
+        assertFalse(req.getSystemHeaders().firstValue(authorization(false)).isPresent());
+        assertEquals(authenticator.COUNTER.get(), 0);
+
+        // Creates the Response to the first request, and call filter.response
+        // with it. That response has a 401 or 407 status code.
+        // The expectation is that the filter will return a new request containing
+        // credentials, and will also cache the credentials in the multi exchange.
+        // The credentials shouldn't be put in the cache until the 200 response
+        // for that request arrives.
+        HttpHeadersImpl headers = new HttpHeadersImpl();
+        headers.addHeader(authenticate(proxy!=null),
+                "Basic realm=\"earth\"");
+        Response response = new Response(req, exchange, headers, unauthorized, v);
+        out.println("Simulating " + unauthorized
+                + " response from " + uri);
+        HttpRequestImpl next = filter.response(response);
+
+        out.println("Checking filter's response to "
+                + unauthorized + " from " + uri);
+        assertTrue(next != null, "next should not be null");
+        String[] up = check(reqURI, next.getSystemHeaders(), proxy);
+        assertEquals(authenticator.COUNTER.get(), 1);
+
+        // Now simulate a new successful exchange to get the credentials in the cache
+        // We first call filter.request with the request that was previously
+        // returned by the filter, then create a new Response with a 200 status
+        // code, and feed that to the filter with filter.response.
+        // At this point, the credentials will be added to the cache.
+        out.println("Simulating next request with credentials to " + uri);
+        exchange = new Exchange<>(next, multi);
+        filter.request(next, multi);
+        out.println("Checking credentials in request header after filter for " + uri);
+        check(reqURI, next.getSystemHeaders(), proxy);
+        check(next.uri(), next.getSystemHeaders(), proxy);
+        out.println("Simulating  successful response 200 from " + uri);
+        response = new Response(next, exchange, new HttpHeadersImpl(), 200, v);
+        next = filter.response(response);
+        assertTrue(next == null, "next should be null");
+        assertEquals(authenticator.COUNTER.get(), 1);
+
+        // Now verify that the cache is used for the next request to the same server.
+        // We're going to create a request to the same server by appending "/bar" to
+        // the original request path. Then we're going to feed that to filter.request
+        // The expectation is that filter.request will add the credentials to the
+        // request system headers, because it should find them in the cache.
+        int fragmentIndex = uri.indexOf('#');
+        String subpath = "/bar";
+        String prefix = uri;
+        String fragment =  "";
+        if (fragmentIndex > -1) {
+            prefix = uri.substring(0, fragmentIndex);
+            fragment = uri.substring(fragmentIndex);
+        }
+        URI reqURI2 = URI.create(prefix + subpath + fragment);
+        out.println("Simulating new request to " + reqURI2);
+        HttpRequestBuilderImpl reqBuilder2 =
+                new HttpRequestBuilderImpl(reqURI2);
+        HttpRequestImpl origReq2 = new HttpRequestImpl(reqBuilder2);
+        HttpRequestImpl req2 = new HttpRequestImpl(origReq2, ps, AccessController.getContext());
+        MultiExchange<?> multi2 = new MultiExchange<Void>(origReq2, req2, client,
+                HttpResponse.BodyHandler.replace(null),
+                null, AccessController.getContext());
+        filter.request(req2, multi2);
+        out.println("Check that filter has added credentials from cache for " + reqURI2
+                + " with proxy " + req2.proxy());
+        String[] up2 = check(reqURI, req2.getSystemHeaders(), proxy);
+        assertTrue(Arrays.deepEquals(up, up2), format("%s:%s != %s:%s", up2[0], up2[1], up[0], up[1]));
+        assertEquals(authenticator.COUNTER.get(), 1);
+
+        // Now verify that the cache is not used if we send a request to a different server.
+        // We're going to append ".bar" to the original request host name, and feed that
+        // to filter.request.
+        // There are actually two cases: if we were using a proxy, then the new request
+        // should contain proxy credentials. If we were not using a proxy, then it should
+        // not contain any credentials at all.
+        URI reqURI3;
+        if (isNullOrEmpty(reqURI.getPath())
+                && isNullOrEmpty(reqURI.getFragment())
+                && reqURI.getPort() == -1) {
+            reqURI3 = URI.create(uri + ".bar");
+        } else {
+            reqURI3 = new URI(reqURI.getScheme(), reqURI.getUserInfo(),
+                         reqURI.getHost() + ".bar", reqURI.getPort(),
+                              reqURI.getPath(), reqURI.getQuery(),
+                              reqURI.getFragment());
+        }
+        out.println("Simulating new request to " + reqURI3);
+        HttpRequestBuilderImpl reqBuilder3 =
+                new HttpRequestBuilderImpl(reqURI3);
+        HttpRequestImpl origReq3 = new HttpRequestImpl(reqBuilder3);
+        HttpRequestImpl req3 = new HttpRequestImpl(origReq3, ps, AccessController.getContext());
+        MultiExchange<?> multi3 = new MultiExchange<Void>(origReq3, req3, client,
+                HttpResponse.BodyHandler.replace(null),
+                null, AccessController.getContext());
+        filter.request(req3, multi3);
+        if (proxy == null) {
+            out.println("Check that filter has not added proxy credentials from cache for " + reqURI3);
+            assert !req3.getSystemHeaders().firstValue(authorization(true)).isPresent()
+                    : format("Unexpected proxy credentials found: %s",
+                    java.util.stream.Stream.of(getAuthorization(req3.getSystemHeaders(), true))
+                            .collect(joining(":")));
+            assertFalse(req3.getSystemHeaders().firstValue(authorization(true)).isPresent());
+        } else {
+            out.println("Check that filter has added proxy credentials from cache for " + reqURI3);
+            String[] up3 = check(reqURI, req3.getSystemHeaders(), proxy);
+            assertTrue(Arrays.deepEquals(up, up3), format("%s:%s != %s:%s", up3[0], up3[1], up[0], up[1]));
+        }
+        out.println("Check that filter has not added server credentials from cache for " + reqURI3);
+        assert !req3.getSystemHeaders().firstValue(authorization(false)).isPresent()
+                : format("Unexpected server credentials found: %s",
+                java.util.stream.Stream.of(getAuthorization(req3.getSystemHeaders(), false))
+                        .collect(joining(":")));
+        assertFalse(req3.getSystemHeaders().firstValue(authorization(false)).isPresent());
+        assertEquals(authenticator.COUNTER.get(), 1);
+
+        // Now we will verify that credentials for proxies are not used for servers and
+        // conversely.
+        // If we were using a proxy, we're now going to send a request to the proxy host,
+        // without using a proxy, and verify that filter.request neither add proxy credential
+        // or server credential to that host.
+        // I we were not using a proxy, we're going to send a request to the original
+        // server, using a proxy whose address matches the original server.
+        // We expect that the cache will add server credentials, but not proxy credentials.
+        int port = reqURI.getPort();
+        port = port == -1 ? defaultPort(reqURI.getScheme()) : port;
+        ProxySelector fakeProxy = proxy == null
+                ? ProxySelector.of(InetSocketAddress.createUnresolved(
+                reqURI.getHost(), port))
+                : NO_PROXY;
+        URI reqURI4 = proxy == null ? reqURI : new URI("http", null, req.proxy().getHostName(),
+                    req.proxy().getPort(), "/", null, null);
+        HttpRequestBuilderImpl reqBuilder4 = new HttpRequestBuilderImpl(reqURI4);
+        HttpRequestImpl origReq4 = new HttpRequestImpl(reqBuilder4);
+        HttpRequestImpl req4 = new HttpRequestImpl(origReq4, fakeProxy,
+                AccessController.getContext());
+        MultiExchange<?> multi4 = new MultiExchange<Void>(origReq4, req4, client,
+                HttpResponse.BodyHandler.replace(null), null,
+                AccessController.getContext());
+        out.println("Simulating new request to " + reqURI4 + " with a proxy " + req4.proxy());
+        assertTrue((req4.proxy() == null) == (proxy != null),
+                "(req4.proxy() == null) == (proxy != null) should be true");
+        filter.request(req4, multi4);
+        out.println("Check that filter has not added proxy credentials from cache for "
+                + reqURI4 + " (proxy: " + req4.proxy()  + ")");
+        assert !req4.getSystemHeaders().firstValue(authorization(true)).isPresent()
+                : format("Unexpected proxy credentials found: %s",
+                java.util.stream.Stream.of(getAuthorization(req4.getSystemHeaders(), true))
+                        .collect(joining(":")));
+        assertFalse(req4.getSystemHeaders().firstValue(authorization(true)).isPresent());
+        if (proxy != null) {
+            out.println("Check that filter has not added server credentials from cache for "
+                    + reqURI4 + " (proxy: " + req4.proxy()  + ")");
+            assert !req4.getSystemHeaders().firstValue(authorization(false)).isPresent()
+                    : format("Unexpected server credentials found: %s",
+                    java.util.stream.Stream.of(getAuthorization(req4.getSystemHeaders(), false))
+                            .collect(joining(":")));
+            assertFalse(req4.getSystemHeaders().firstValue(authorization(false)).isPresent());
+        } else {
+            out.println("Check that filter has added server credentials from cache for "
+                    + reqURI4 + " (proxy: " + req4.proxy()  + ")");
+            String[] up4 = check(reqURI, req4.getSystemHeaders(), proxy);
+            assertTrue(Arrays.deepEquals(up, up4),  format("%s:%s != %s:%s", up4[0], up4[1], up[0], up[1]));
+        }
+        assertEquals(authenticator.COUNTER.get(), 1);
+
+        if (proxy != null) {
+            // Now if we were using a proxy, we're going to send the same request than
+            // the original request, but without a proxy, and verify that this time
+            // the cache does not add any server or proxy credential. It should not
+            // add server credential because it should not have them (we only used
+            // proxy authentication so far) and it should not add proxy credentials
+            // because the request has no proxy.
+            HttpRequestBuilderImpl reqBuilder5 = new HttpRequestBuilderImpl(reqURI);
+            HttpRequestImpl origReq5 = new HttpRequestImpl(reqBuilder5);
+            HttpRequestImpl req5 = new HttpRequestImpl(origReq5, NO_PROXY,
+                    AccessController.getContext());
+            MultiExchange<?> multi5 = new MultiExchange<Void>(origReq5, req5, client,
+                    HttpResponse.BodyHandler.replace(null), null,
+                    AccessController.getContext());
+            out.println("Simulating new request to " + reqURI + " with a proxy " + req5.proxy());
+            assertTrue(req5.proxy() == null, "req5.proxy() should be null");
+            Exchange<?> exchange5 = new Exchange<>(req5, multi5);
+            filter.request(req5, multi5);
+            out.println("Check that filter has not added server credentials from cache for "
+                    + reqURI + " (proxy: " + req5.proxy()  + ")");
+            assert !req5.getSystemHeaders().firstValue(authorization(false)).isPresent()
+                    : format("Unexpected server credentials found: %s",
+                    java.util.stream.Stream.of(getAuthorization(req5.getSystemHeaders(), false))
+                            .collect(joining(":")));
+            assertFalse(req5.getSystemHeaders().firstValue(authorization(false)).isPresent());
+            out.println("Check that filter has not added proxy credentials from cache for "
+                    + reqURI + " (proxy: " + req5.proxy()  + ")");
+            assert !req5.getSystemHeaders().firstValue(authorization(true)).isPresent()
+                    : format("Unexpected proxy credentials found: %s",
+                    java.util.stream.Stream.of(getAuthorization(req5.getSystemHeaders(), true))
+                            .collect(joining(":")));
+            assertFalse(req5.getSystemHeaders().firstValue(authorization(true)).isPresent());
+            assertEquals(authenticator.COUNTER.get(), 1);
+
+            // Now simulate a 401 response from the server
+            HttpHeadersImpl headers5 = new HttpHeadersImpl();
+            headers5.addHeader(authenticate(false),
+                    "Basic realm=\"earth\"");
+            unauthorized = 401;
+            Response response5 = new Response(req5, exchange5, headers5, unauthorized, v);
+            out.println("Simulating " + unauthorized
+                    + " response from " + uri);
+            HttpRequestImpl next5 = filter.response(response5);
+            assertEquals(authenticator.COUNTER.get(), 2);
+
+            out.println("Checking filter's response to "
+                    + unauthorized + " from " + uri);
+            assertTrue(next5 != null, "next5 should not be null");
+            String[] up5 = check(reqURI, next5.getSystemHeaders(), null);
+
+            // now simulate a 200 response from the server
+            exchange5 = new Exchange<>(next5, multi5);
+            filter.request(next5, multi5);
+            response5 = new Response(next5, exchange5, new HttpHeadersImpl(), 200, v);
+            filter.response(response5);
+            assertEquals(authenticator.COUNTER.get(), 2);
+
+            // now send the request again, with proxy this time, and it should have both
+            // server auth and proxy auth
+            HttpRequestBuilderImpl reqBuilder6 = new HttpRequestBuilderImpl(reqURI);
+            HttpRequestImpl origReq6 = new HttpRequestImpl(reqBuilder6);
+            HttpRequestImpl req6 = new HttpRequestImpl(origReq6, ps,
+                    AccessController.getContext());
+            MultiExchange<?> multi6 = new MultiExchange<Void>(origReq6, req6, client,
+                    HttpResponse.BodyHandler.replace(null), null,
+                    AccessController.getContext());
+            out.println("Simulating new request to " + reqURI + " with a proxy " + req6.proxy());
+            assertTrue(req6.proxy() != null, "req6.proxy() should not be null");
+            Exchange<?> exchange6 = new Exchange<>(req6, multi6);
+            filter.request(req6, multi6);
+            out.println("Check that filter has added server credentials from cache for "
+                    + reqURI + " (proxy: " + req6.proxy()  + ")");
+            check(reqURI, req6.getSystemHeaders(), null);
+            out.println("Check that filter has added proxy credentials from cache for "
+                    + reqURI + " (proxy: " + req6.proxy()  + ")");
+            String[] up6 = check(reqURI, req6.getSystemHeaders(), proxy);
+            assertTrue(Arrays.deepEquals(up, up6), format("%s:%s != %s:%s", up6[0], up6[1], up[0], up[1]));
+            assertEquals(authenticator.COUNTER.get(), 2);
+        }
+
+        if (proxy == null && uri.contains("x/y/z")) {
+            URI reqURI7 = URI.create(prefix + "/../../w/z" + fragment);
+            assertTrue(reqURI7.getPath().contains("../../"));
+            HttpRequestBuilderImpl reqBuilder7 = new HttpRequestBuilderImpl(reqURI7);
+            HttpRequestImpl origReq7 = new HttpRequestImpl(reqBuilder7);
+            HttpRequestImpl req7 = new HttpRequestImpl(origReq7, ps,
+                    AccessController.getContext());
+            MultiExchange<?> multi7 = new MultiExchange<Void>(origReq7, req7, client,
+                    HttpResponse.BodyHandler.replace(null), null,
+                    AccessController.getContext());
+            out.println("Simulating new request to " + reqURI7 + " with a proxy " + req7.proxy());
+            assertTrue(req7.proxy() == null, "req7.proxy() should be null");
+            Exchange<?> exchange7 = new Exchange<>(req7, multi7);
+            filter.request(req7, multi7);
+            out.println("Check that filter has not added server credentials from cache for "
+                    + reqURI7 + " (proxy: " + req7.proxy()  + ") [resolved uri: "
+                    + reqURI7.resolve(".") + " should not match " + reqURI.resolve(".") + "]");
+            assert !req7.getSystemHeaders().firstValue(authorization(false)).isPresent()
+                    : format("Unexpected server credentials found: %s",
+                    java.util.stream.Stream.of(getAuthorization(req7.getSystemHeaders(), false))
+                            .collect(joining(":")));
+            assertFalse(req7.getSystemHeaders().firstValue(authorization(false)).isPresent());
+            out.println("Check that filter has not added proxy credentials from cache for "
+                    + reqURI7 + " (proxy: " + req7.proxy()  + ")");
+            assert !req7.getSystemHeaders().firstValue(authorization(true)).isPresent()
+                    : format("Unexpected proxy credentials found: %s",
+                    java.util.stream.Stream.of(getAuthorization(req7.getSystemHeaders(), true))
+                            .collect(joining(":")));
+            assertFalse(req7.getSystemHeaders().firstValue(authorization(true)).isPresent());
+            assertEquals(authenticator.COUNTER.get(), 1);
+
+        }
+
+        Reference.reachabilityFence(facade);
+    }
+
+    static int defaultPort(String protocol) {
+        if ("http".equalsIgnoreCase(protocol)) return 80;
+        if ("https".equalsIgnoreCase(protocol)) return 443;
+        return -1;
+    }
+
+    static String authenticate(boolean proxy) {
+        return proxy ? "Proxy-Authenticate" : "WWW-Authenticate";
+    }
+    static String authorization(boolean proxy) {
+        return proxy ? "Proxy-Authorization" : "Authorization";
+    }
+
+    static String[] getAuthorization(HttpHeaders headers, boolean proxy) {
+        String auth = headers.firstValue(authorization(proxy)).get().substring(6);
+        String pw = new String(Base64.getDecoder().decode(auth), US_ASCII);
+        String[] up = pw.split(":");
+        up[1] = new String(Base64.getDecoder().decode(up[1]), US_ASCII);
+        return up;
+    }
+
+    static Authenticator.RequestorType requestorType(boolean proxy) {
+        return proxy ? Authenticator.RequestorType.PROXY
+                     : Authenticator.RequestorType.SERVER;
+    }
+
+    static String[] check(URI reqURI, HttpHeaders headers, String proxy) throws Exception {
+        out.println("Next request headers: " + headers.map());
+        String[] up = getAuthorization(headers, proxy != null);
+        String u = up[0];
+        String p = up[1];
+        out.println("user:password: " + u + ":" + p);
+        String protocol = proxy != null ? "http" : reqURI.getScheme();
+        String expectedUser = "u." + protocol;
+        assertEquals(u, expectedUser);
+        String host = proxy == null ? reqURI.getHost() :
+                proxy.substring(0, proxy.lastIndexOf(':'));
+        int port = proxy == null ? reqURI.getPort()
+                : Integer.parseInt(proxy.substring(proxy.lastIndexOf(':')+1));
+        String expectedPw = concat(requestorType(proxy!=null),
+                "basic", protocol, host,
+                port, "earth", reqURI.toURL());
+        assertEquals(p, expectedPw);
+        return new String[] {u, p};
+    }
+
+    static String concat(Authenticator.RequestorType reqType,
+                           String authScheme,
+                           String requestingProtocol,
+                           String requestingHost,
+                           int requestingPort,
+                           String realm,
+                           URL requestingURL) {
+        return new StringBuilder()
+                .append(reqType).append(":")
+                .append(authScheme).append(":")
+                .append(String.valueOf(realm))
+                .append("[")
+                .append(requestingProtocol).append(':')
+                .append(requestingHost).append(':')
+                .append(requestingPort).append("]")
+                .append("/").append(String.valueOf(requestingURL))
+                .toString();
+    }
+
+    static class TestAuthenticator extends Authenticator {
+        final AtomicLong COUNTER = new AtomicLong();
+        @Override
+        public PasswordAuthentication getPasswordAuthentication() {
+            COUNTER.incrementAndGet();
+            return new PasswordAuthentication("u."+getRequestingProtocol(),
+                    Base64.getEncoder().encodeToString(concat().getBytes(US_ASCII))
+                            .toCharArray());
+        }
+
+        String concat() {
+            return AuthenticationFilterTest.concat(
+                    getRequestorType(),
+                    getRequestingScheme(),
+                    getRequestingProtocol(),
+                    getRequestingHost(),
+                    getRequestingPort(),
+                    getRequestingPrompt(),
+                    getRequestingURL());
+        }
+
+    }
+}