http-client-branch: make it possible to supply proxy-authorization headers http-client-branch
authordfuchs
Fri, 26 Jan 2018 16:46:52 +0000
branchhttp-client-branch
changeset 56041 b4b5e09ef3cc
parent 56040 f8eabb9a5c0f
child 56042 40d7b06bb6e9
http-client-branch: make it possible to supply proxy-authorization headers
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLTunnelConnection.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AuthenticationFilter.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Exchange.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Exchange.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Request.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpHeaders.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponseImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ImmutableHeaders.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ProxyAuthenticationRequired.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Stream.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Utils.java
test/jdk/java/net/httpclient/DigestEchoClient.java
test/jdk/java/net/httpclient/DigestEchoServer.java
test/jdk/java/net/httpclient/MultiAuthTest.java
test/jdk/java/net/httpclient/RequestBuilderTest.java
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLTunnelConnection.java	Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLTunnelConnection.java	Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 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
@@ -45,10 +45,11 @@
     AsyncSSLTunnelConnection(InetSocketAddress addr,
                              HttpClientImpl client,
                              String[] alpn,
-                             InetSocketAddress proxy)
+                             InetSocketAddress proxy,
+                             HttpHeaders proxyHeaders)
     {
         super(addr, client, Utils.getServerName(addr), alpn);
-        this.plainConnection = new PlainTunnelingConnection(addr, proxy, client);
+        this.plainConnection = new PlainTunnelingConnection(addr, proxy, client, proxyHeaders);
         this.writePublisher = new PlainHttpPublisher();
     }
 
@@ -70,6 +71,9 @@
     }
 
     @Override
+    boolean isTunnel() { return true; }
+
+    @Override
     boolean connected() {
         return plainConnection.connected(); // && sslDelegate.connected();
     }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AuthenticationFilter.java	Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AuthenticationFilter.java	Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 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
@@ -26,10 +26,12 @@
 package jdk.incubator.http;
 
 import java.io.IOException;
+import java.net.MalformedURLException;
 import java.net.PasswordAuthentication;
 import java.net.URI;
 import java.net.InetSocketAddress;
 import java.net.URISyntaxException;
+import java.net.URL;
 import java.util.Base64;
 import java.util.LinkedList;
 import java.util.Objects;
@@ -72,6 +74,7 @@
 
         String realm = parser.findValue("realm");
         java.net.Authenticator.RequestorType rtype = proxy ? PROXY : SERVER;
+        URL url = toURL(uri, req.method(), proxy);
 
         // needs to be instance method in Authenticator
         return auth.requestPasswordAuthenticationInstance(uri.getHost(),
@@ -80,11 +83,21 @@
                                                           uri.getScheme(),
                                                           realm,
                                                           authscheme,
-                                                          uri.toURL(),
+                                                          url,
                                                           rtype
         );
     }
 
+    private URL toURL(URI uri, String method, boolean proxy)
+            throws MalformedURLException
+    {
+        if (proxy && "CONNECT".equalsIgnoreCase(method)
+                && "socket".equalsIgnoreCase(uri.getScheme())) {
+            return null; // proxy tunneling
+        }
+        return uri.toURL();
+    }
+
     private URI getProxyURI(HttpRequestImpl r) {
         InetSocketAddress proxy = r.proxy();
         if (proxy == null) {
@@ -221,6 +234,9 @@
 
         AuthInfo au = proxy ? exchange.proxyauth : exchange.serverauth;
         if (au == null) {
+            // if no authenticator, let the user deal with 407/401
+            if (!exchange.client().authenticator().isPresent()) return null;
+
             PasswordAuthentication pw = getCredentials(authval, proxy, req);
             if (pw == null) {
                 throw new IOException("No credentials provided");
@@ -242,6 +258,10 @@
             if (au.fromcache) {
                 cache.remove(au.cacheEntry);
             }
+
+            // if no authenticator, let the user deal with 407/401
+            if (!exchange.client().authenticator().isPresent()) return null;
+
             // try again
             PasswordAuthentication pw = getCredentials(authval, proxy, req);
             if (pw == null) {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Exchange.java	Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Exchange.java	Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 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
@@ -37,6 +37,8 @@
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
+import java.util.function.Function;
+
 import jdk.incubator.http.internal.common.MinimalFuture;
 import jdk.incubator.http.internal.common.Utils;
 import jdk.incubator.http.internal.common.Log;
@@ -65,6 +67,8 @@
     final HttpClientImpl client;
     volatile ExchangeImpl<T> exchImpl;
     volatile CompletableFuture<? extends ExchangeImpl<T>> exchangeCF;
+    volatile CompletableFuture<Void> bodyIgnored;
+
     // used to record possible cancellation raised before the exchImpl
     // has been established.
     private volatile IOException failed;
@@ -119,6 +123,12 @@
 
 
     public CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler) {
+        // If we received a 407 while establishing the exchange
+        // there will be no body to read: bodyIgnored will be true,
+        // and exchImpl will be null (if we were trying to establish
+        // an HTTP/2 tunnel through an HTTP/1.1 proxy)
+        if (bodyIgnored != null) return MinimalFuture.completedFuture(null);
+
         // The connection will not be returned to the pool in the case of WebSocket
         return exchImpl.readBodyAsync(handler, !request.isWebSocket(), parentExecutor)
                 .whenComplete((r,t) -> exchImpl.completed());
@@ -131,6 +141,7 @@
      * other cases.
      */
     public CompletableFuture<Void> ignoreBody() {
+        if (bodyIgnored != null) return bodyIgnored;
         return exchImpl.ignoreBody();
     }
 
@@ -287,45 +298,92 @@
         }
     }
 
+    // check whether the headersSentCF was completed exceptionally with
+    // ProxyAuthorizationRequired. If so the Response embedded in the
+    // exception is returned. Otherwise we proceed.
+    private CompletableFuture<Response> checkFor407(ExchangeImpl<T> ex, Throwable t,
+                                                    Function<ExchangeImpl<T>,CompletableFuture<Response>> andThen) {
+        t = Utils.getCompletionCause(t);
+        if (t instanceof ProxyAuthenticationRequired) {
+            bodyIgnored = MinimalFuture.completedFuture(null);
+            Response proxyResponse = ((ProxyAuthenticationRequired)t).proxyResponse;
+            Response syntheticResponse = new Response(request, this,
+                    proxyResponse.headers, proxyResponse.statusCode, proxyResponse.version);
+            return MinimalFuture.completedFuture(syntheticResponse);
+        } else if (t != null) {
+            return MinimalFuture.failedFuture(t);
+        } else {
+            return andThen.apply(ex);
+        }
+    }
+
+    // After sending the request headers, if no ProxyAuthorizationRequired
+    // was raised and the expectContinue flag is on, we need to wait
+    // for the 100-Continue response
+    private CompletableFuture<Response> expectContinue(ExchangeImpl<T> ex) {
+        assert request.expectContinue();
+        return ex.getResponseAsync(parentExecutor)
+                .thenCompose((Response r1) -> {
+            Log.logResponse(r1::toString);
+            int rcode = r1.statusCode();
+            if (rcode == 100) {
+                Log.logTrace("Received 100-Continue: sending body");
+                CompletableFuture<Response> cf =
+                        exchImpl.sendBodyAsync()
+                                .thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
+                cf = wrapForUpgrade(cf);
+                cf = wrapForLog(cf);
+                return cf;
+            } else {
+                Log.logTrace("Expectation failed: Received {0}",
+                        rcode);
+                if (upgrading && rcode == 101) {
+                    IOException failed = new IOException(
+                            "Unable to handle 101 while waiting for 100");
+                    return MinimalFuture.failedFuture(failed);
+                }
+                return exchImpl.readBodyAsync(this::ignoreBody, false, parentExecutor)
+                        .thenApply(v ->  r1);
+            }
+        });
+    }
+
+    // After sending the request headers, if no ProxyAuthorizationRequired
+    // was raised and the expectContinue flag is off, we can immediately
+    // send the request body and proceed.
+    private CompletableFuture<Response> sendRequestBody(ExchangeImpl<T> ex) {
+        assert !request.expectContinue();
+        CompletableFuture<Response> cf = ex.sendBodyAsync()
+                .thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
+        cf = wrapForUpgrade(cf);
+        cf = wrapForLog(cf);
+        return cf;
+    }
+
     CompletableFuture<Response> responseAsyncImpl0(HttpConnection connection) {
+        Function<ExchangeImpl<T>, CompletableFuture<Response>> after407Check;
+        bodyIgnored = null;
         if (request.expectContinue()) {
             request.addSystemHeader("Expect", "100-Continue");
             Log.logTrace("Sending Expect: 100-Continue");
-            return establishExchange(connection)
-                    .thenCompose((ex) -> ex.sendHeadersAsync())
-                    .thenCompose(v -> exchImpl.getResponseAsync(parentExecutor))
-                    .thenCompose((Response r1) -> {
-                        Log.logResponse(r1::toString);
-                        int rcode = r1.statusCode();
-                        if (rcode == 100) {
-                            Log.logTrace("Received 100-Continue: sending body");
-                            CompletableFuture<Response> cf =
-                                    exchImpl.sendBodyAsync()
-                                            .thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
-                            cf = wrapForUpgrade(cf);
-                            cf = wrapForLog(cf);
-                            return cf;
-                        } else {
-                            Log.logTrace("Expectation failed: Received {0}",
-                                         rcode);
-                            if (upgrading && rcode == 101) {
-                                IOException failed = new IOException(
-                                        "Unable to handle 101 while waiting for 100");
-                                return MinimalFuture.failedFuture(failed);
-                            }
-                            return exchImpl.readBodyAsync(this::ignoreBody, false, parentExecutor)
-                                  .thenApply(v ->  r1);
-                        }
-                    });
+            // wait for 100-Continue before sending body
+            after407Check = this::expectContinue;
         } else {
-            CompletableFuture<Response> cf = establishExchange(connection)
-                    .thenCompose((ex) -> ex.sendHeadersAsync())
-                    .thenCompose(ExchangeImpl::sendBodyAsync)
-                    .thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
-            cf = wrapForUpgrade(cf);
-            cf = wrapForLog(cf);
-            return cf;
+            // send request body and proceed.
+            after407Check = this::sendRequestBody;
         }
+        // The ProxyAuthorizationRequired can be triggered either by
+        // establishExchange (case of HTTP/2 SSL tunelling through HTTP/1.1 proxy
+        // or by sendHeaderAsync (case of HTTP/1.1 SSL tunelling through HTTP/1.1 proxy
+        // Therefore we handle it with a call to this checkFor407(...) after these
+        // two places.
+        Function<ExchangeImpl<T>, CompletableFuture<Response>> afterExch407Check =
+                (ex) -> ex.sendHeadersAsync()
+                        .handle((r,t) -> this.checkFor407(r, t, after407Check))
+                        .thenCompose(Function.identity());
+        return establishExchange(connection)
+                .handle((r,t) -> this.checkFor407(r,t, afterExch407Check))
+                .thenCompose(Function.identity());
     }
 
     private CompletableFuture<Response> wrapForUpgrade(CompletableFuture<Response> cf) {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Exchange.java	Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Exchange.java	Fri Jan 26 16:46:52 2018 +0000
@@ -82,9 +82,9 @@
     private final Http1Publisher writePublisher = new Http1Publisher();
 
     /** Completed when the header have been published, or there is an error */
-    private volatile CompletableFuture<ExchangeImpl<T>> headersSentCF  = new MinimalFuture<>();
+    private final CompletableFuture<ExchangeImpl<T>> headersSentCF  = new MinimalFuture<>();
      /** Completed when the body has been published, or there is an error */
-    private volatile CompletableFuture<ExchangeImpl<T>> bodySentCF = new MinimalFuture<>();
+    private final CompletableFuture<ExchangeImpl<T>> bodySentCF = new MinimalFuture<>();
 
     /** The subscriber to the request's body published. Maybe null. */
     private volatile Http1BodySubscriber bodySubscriber;
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Request.java	Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Request.java	Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 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
@@ -35,6 +35,7 @@
 import java.net.InetSocketAddress;
 import java.util.Objects;
 import java.util.concurrent.Flow;
+import java.util.function.Predicate;
 
 import jdk.incubator.http.Http1Exchange.Http1BodySubscriber;
 import jdk.incubator.http.internal.common.HttpHeadersImpl;
@@ -82,14 +83,26 @@
     }
 
     private void collectHeaders0(StringBuilder sb) {
-        collectHeaders1(sb, systemHeaders);
-        collectHeaders1(sb, userHeaders);
+        Predicate<String> filter = connection.isTunnel()
+                ? Utils.NO_PROXY_HEADER : Utils.ALL_HEADERS;
+
+        // If we're sending this request through a tunnel,
+        // then don't send any preemptive proxy-* headers that
+        // the authentication filter may have saved in its
+        // cache.
+        collectHeaders1(sb, systemHeaders, filter);
+
+        // If we're sending this request through a tunnel,
+        // don't send any user-supplied proxy-* headers
+        // to the target server.
+        collectHeaders1(sb, userHeaders, filter);
         sb.append("\r\n");
     }
 
-    private void collectHeaders1(StringBuilder sb, HttpHeaders headers) {
+    private void collectHeaders1(StringBuilder sb, HttpHeaders headers, Predicate<String> filter) {
         for (Map.Entry<String,List<String>> entry : headers.map().entrySet()) {
             String key = entry.getKey();
+            if (!filter.test(key)) continue;
             List<String> values = entry.getValue();
             for (String value : values) {
                 sb.append(key).append(": ").append(value).append("\r\n");
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java	Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java	Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 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
@@ -34,6 +34,7 @@
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.TreeMap;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 import java.util.concurrent.ConcurrentLinkedDeque;
@@ -186,7 +187,7 @@
                 if (version == HTTP_2) {
                     alpn = new String[] { "h2", "http/1.1" };
                 }
-                return getSSLConnection(addr, proxy, alpn, client);
+                return getSSLConnection(addr, proxy, alpn, request, client);
             }
         }
     }
@@ -194,13 +195,26 @@
     private static HttpConnection getSSLConnection(InetSocketAddress addr,
                                                    InetSocketAddress proxy,
                                                    String[] alpn,
+                                                   HttpRequestImpl request,
                                                    HttpClientImpl client) {
         if (proxy != null)
-            return new AsyncSSLTunnelConnection(addr, client, alpn, proxy);
+            return new AsyncSSLTunnelConnection(addr, client, alpn, proxy,
+                                                proxyHeaders(request));
         else
             return new AsyncSSLConnection(addr, client, alpn);
     }
 
+    // Composes a new immutable HttpHeaders that combines the
+    // user and system header but only keeps those headers that
+    // start with "proxy-"
+    private static HttpHeaders proxyHeaders(HttpRequestImpl request) {
+        Map<String, List<String>> combined = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        combined.putAll(request.getSystemHeaders().map());
+        combined.putAll(request.headers().map()); // let user override system
+        // keep only proxy-*
+        return ImmutableHeaders.of(combined, Utils.IS_PROXY_HEADER);
+    }
+
     /* Returns either a plain HTTP connection or a plain tunnelling connection
      * for proxied WebSocket */
     private static HttpConnection getPlainConnection(InetSocketAddress addr,
@@ -208,7 +222,8 @@
                                                      HttpRequestImpl request,
                                                      HttpClientImpl client) {
         if (request.isWebSocket() && proxy != null)
-            return new PlainTunnelingConnection(addr, proxy, client);
+            return new PlainTunnelingConnection(addr, proxy, client,
+                                                proxyHeaders(request));
 
         if (proxy == null)
             return new PlainHttpConnection(addr, client);
@@ -243,6 +258,9 @@
         }
     }
 
+    /* Tells whether or not this connection is a tunnel through a proxy */
+    boolean isTunnel() { return false; }
+
     abstract SocketChannel channel();
 
     final InetSocketAddress address() {
@@ -251,11 +269,6 @@
 
     abstract ConnectionPool.CacheKey cacheKey();
 
-//    // overridden in SSL only
-//    SSLParameters sslParameters() {
-//        return null;
-//    }
-
     /**
      * Closes this connection, by returning the socket to its connection pool.
      */
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpHeaders.java	Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpHeaders.java	Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 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
@@ -156,7 +156,7 @@
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
-        sb.append(super.toString()).append(" ");
+        sb.append(super.toString()).append(" { ");
         sb.append(map());
         sb.append(" }");
         return sb.toString();
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java	Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java	Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 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
@@ -140,13 +140,14 @@
     }
 
     /* used for creating CONNECT requests  */
-    HttpRequestImpl(String method, InetSocketAddress authority) {
+    HttpRequestImpl(String method, InetSocketAddress authority, HttpHeaders headers) {
         // TODO: isWebSocket flag is not specified, but the assumption is that
         // such a request will never be made on a connection that will be returned
         // to the connection pool (we might need to revisit this constructor later)
+        assert "CONNECT".equalsIgnoreCase(method);
         this.method = method;
         this.systemHeaders = new HttpHeadersImpl();
-        this.userHeaders = ImmutableHeaders.empty();
+        this.userHeaders = ImmutableHeaders.of(headers);
         this.uri = URI.create("socket://" + authority.getHostString() + ":"
                               + Integer.toString(authority.getPort()) + "/");
         this.proxy = null;
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponseImpl.java	Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponseImpl.java	Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 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
@@ -66,31 +66,18 @@
         this.sslParameters = exch.client().sslParameters();
         this.uri = response.request().uri();
         this.version = response.version();
-        this.connection = exch.exchImpl.connection();
+        this.connection = connection(exch);
         this.stream = null;
         this.body = body;
     }
 
-//    // A response to a PUSH_PROMISE
-//    public HttpResponseImpl(Response response,
-//                            HttpRequestImpl pushRequest,
-//                            ImmutableHeaders headers,
-//                            Stream<T> stream,
-//                            SSLParameters sslParameters,
-//                            T body) {
-//        this.responseCode = response.statusCode();
-//        this.exchange = null;
-//        this.initialRequest = null; // ## fix this
-//        this.finalRequest = pushRequest;
-//        this.headers = headers;
-//        //this.trailers = null;
-//        this.sslParameters = sslParameters;
-//        this.uri = finalRequest.uri(); // TODO: take from headers
-//        this.version = HttpClient.Version.HTTP_2;
-//        this.connection = stream.connection();
-//        this.stream = stream;
-//        this.body = body;
-//    }
+    private HttpConnection connection(Exchange<?> exch) {
+        if (exch == null || exch.exchImpl == null) {
+            assert responseCode == 407;
+            return null; // case of Proxy 407
+        }
+        return exch.exchImpl.connection();
+    }
 
     private ExchangeImpl<?> exchangeImpl() {
         return exchange != null ? exchange.exchImpl : stream;
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ImmutableHeaders.java	Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ImmutableHeaders.java	Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 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
@@ -47,6 +47,12 @@
         return of(src, x -> true);
     }
 
+    public static ImmutableHeaders of(HttpHeaders headers) {
+        return (headers instanceof ImmutableHeaders)
+                ? (ImmutableHeaders)headers
+                : of(headers.map());
+    }
+
     public static ImmutableHeaders of(Map<String, List<String>> src,
                                       Predicate<? super String> keyAllowed) {
         requireNonNull(src, "src");
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java	Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java	Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 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
@@ -31,8 +31,11 @@
 import java.nio.ByteBuffer;
 import java.nio.channels.SocketChannel;
 import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+
 import jdk.incubator.http.internal.common.FlowTube;
 import jdk.incubator.http.internal.common.MinimalFuture;
+
 import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
 
 /**
@@ -43,14 +46,17 @@
 final class PlainTunnelingConnection extends HttpConnection {
 
     final PlainHttpConnection delegate;
+    final HttpHeaders proxyHeaders;
     protected final InetSocketAddress proxyAddr;
     private volatile boolean connected;
 
     protected PlainTunnelingConnection(InetSocketAddress addr,
                                        InetSocketAddress proxy,
-                                       HttpClientImpl client) {
+                                       HttpClientImpl client,
+                                       HttpHeaders proxyHeaders) {
         super(addr, client);
         this.proxyAddr = proxy;
+        this.proxyHeaders = proxyHeaders;
         delegate = new PlainHttpConnection(proxy, client);
     }
 
@@ -62,8 +68,9 @@
                 debug.log(Level.DEBUG, "sending HTTP/1.1 CONNECT");
                 HttpClientImpl client = client();
                 assert client != null;
-                HttpRequestImpl req = new HttpRequestImpl("CONNECT", address);
-                MultiExchange<Void> mulEx = new MultiExchange<>(null, req, client, discard(null), null, null);
+                HttpRequestImpl req = new HttpRequestImpl("CONNECT", address, proxyHeaders);
+                MultiExchange<Void> mulEx = new MultiExchange<>(null, req,
+                        client, discard(null), null, null);
                 Exchange<Void> connectExchange = new Exchange<>(req, mulEx);
 
                 return connectExchange
@@ -71,7 +78,18 @@
                         .thenCompose((Response resp) -> {
                             CompletableFuture<Void> cf = new MinimalFuture<>();
                             debug.log(Level.DEBUG, "got response: %d", resp.statusCode());
-                            if (resp.statusCode() != 200) {
+                            if (resp.statusCode() == 407) {
+                                return connectExchange.ignoreBody().handle((r,t) -> {
+                                    // close delegate after reading body: we won't
+                                    // be reusing that connection anyway.
+                                    delegate.close();
+                                    ProxyAuthenticationRequired authenticationRequired =
+                                            new ProxyAuthenticationRequired(resp);
+                                    cf.completeExceptionally(authenticationRequired);
+                                    return cf;
+                                }).thenCompose(Function.identity());
+                            } else if (resp.statusCode() != 200) {
+                                delegate.close();
                                 cf.completeExceptionally(new IOException(
                                         "Tunnel failed, got: "+ resp.statusCode()));
                             } else {
@@ -88,6 +106,9 @@
     }
 
     @Override
+    boolean isTunnel() { return true; }
+
+    @Override
     HttpPublisher publisher() { return delegate.publisher(); }
 
     @Override
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ProxyAuthenticationRequired.java	Fri Jan 26 16:46:52 2018 +0000
@@ -0,0 +1,48 @@
+/*
+ * 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.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.incubator.http;
+import java.io.IOException;
+
+/**
+ * Signals that a proxy has refused a CONNECT request with a
+ * 407 error code.
+ */
+final class ProxyAuthenticationRequired extends IOException {
+    private static final long serialVersionUID = 0;
+    final transient Response proxyResponse;
+
+    /**
+     * Constructs a {@code ConnectionExpiredException} with the specified detail
+     * message and cause.
+     *
+     * @param   proxyResponse the response from the proxy
+     */
+    public ProxyAuthenticationRequired(Response proxyResponse) {
+        super("Proxy Authentication Required");
+        assert proxyResponse.statusCode() == 407;
+        this.proxyResponse = proxyResponse;
+    }
+}
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Stream.java	Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Stream.java	Fri Jan 26 16:46:52 2018 +0000
@@ -481,7 +481,9 @@
             h.setHeader("content-length", Long.toString(contentLength));
         }
         setPseudoHeaderFields();
-        OutgoingHeaders<Stream<T>> f = new OutgoingHeaders<>(h, request.getUserHeaders(), this);
+        HttpHeaders sysh = filter(h);
+        HttpHeaders userh = filter(request.getUserHeaders());
+        OutgoingHeaders<Stream<T>> f = new OutgoingHeaders<>(sysh, userh, this);
         if (contentLength == 0) {
             f.setFlag(HeadersFrame.END_STREAM);
             endStreamSent = true;
@@ -489,6 +491,20 @@
         return f;
     }
 
+    private HttpHeaders filter(HttpHeaders headers) {
+        if (connection().isTunnel()) {
+            boolean needsFiltering = headers
+                    .firstValue("proxy-authorization")
+                    .isPresent();
+            // don't send proxy-* headers to the target server.
+            if (needsFiltering) {
+                return ImmutableHeaders.of(headers.map(),
+                        Utils.NO_PROXY_HEADER);
+            }
+        }
+        return headers;
+    }
+
     private void setPseudoHeaderFields() {
         HttpHeadersImpl hdrs = requestPseudoHeaders;
         String method = request.method();
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Utils.java	Fri Jan 26 11:08:42 2018 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/common/Utils.java	Fri Jan 26 16:46:52 2018 +0000
@@ -102,12 +102,19 @@
         TreeSet<String> treeSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
         treeSet.addAll(Set.of("connection", "content-length",
                 "date", "expect", "from", "host", "origin",
-                "proxy-authorization", "referer", "upgrade",
+                "referer", "upgrade",
                 "via", "warning"));
         DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet);
     }
+
     public static final Predicate<String>
-        ALLOWED_HEADERS = header -> !Utils.DISALLOWED_HEADERS_SET.contains(header);
+        ALLOWED_HEADERS = header -> !DISALLOWED_HEADERS_SET.contains(header);
+
+    public static final Predicate<String> IS_PROXY_HEADER = (k) ->
+            k != null && k.length() > 6 && "proxy-".equalsIgnoreCase(k.substring(0,6));
+    public static final Predicate<String> NO_PROXY_HEADER =
+            IS_PROXY_HEADER.negate();
+    public static final Predicate<String> ALL_HEADERS = (s) -> true;
 
     public static ByteBuffer getBuffer() {
         return ByteBuffer.allocate(BUFSIZE);
--- a/test/jdk/java/net/httpclient/DigestEchoClient.java	Fri Jan 26 11:08:42 2018 +0000
+++ b/test/jdk/java/net/httpclient/DigestEchoClient.java	Fri Jan 26 16:46:52 2018 +0000
@@ -26,7 +26,6 @@
 import java.math.BigInteger;
 import java.net.ProxySelector;
 import java.net.URI;
-import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
 import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
@@ -35,7 +34,6 @@
 import java.util.List;
 import java.util.Optional;
 import java.util.Random;
-import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.ConcurrentHashMap;
@@ -55,7 +53,7 @@
 import sun.net.www.HeaderParser;
 import static java.lang.System.out;
 import static java.lang.String.format;
-import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
+import static jdk.incubator.http.HttpResponse.BodyHandler.asLines;
 
 /**
  * @test
@@ -151,6 +149,7 @@
             throw new ExceptionInInitializerError(x);
         }
     }
+    static final List<Boolean> BOOLEANS = List.of(true, false);
 
     final ServerSocketFactory factory;
     final boolean useSSL;
@@ -228,7 +227,14 @@
                             authScheme,
                             authType);
                     for (Version version : HttpClient.Version.values()) {
-                        dec.testBasic(version, true);
+                        for (boolean expectContinue : BOOLEANS) {
+                            for (boolean async : BOOLEANS) {
+                                for (boolean preemptive : BOOLEANS) {
+                                    dec.testBasic(version, async,
+                                            expectContinue, preemptive);
+                                }
+                            }
+                        }
                     }
                 }
                 EnumSet<DigestEchoServer.HttpAuthSchemeType> digests =
@@ -238,7 +244,11 @@
                             authScheme,
                             authType);
                     for (Version version : HttpClient.Version.values()) {
-                        dec.testDigest(version, true);
+                        for (boolean expectContinue : BOOLEANS) {
+                            for (boolean async : BOOLEANS) {
+                                dec.testDigest(version, async, expectContinue);
+                            }
+                        }
                     }
                 }
             }
@@ -259,11 +269,18 @@
     final static AtomicLong basics = new AtomicLong();
     final static AtomicLong basicCount = new AtomicLong();
     // @Test
-    void testBasic(HttpClient.Version version, boolean async)
+    void testBasic(HttpClient.Version version, boolean async,
+                   boolean expectContinue, boolean preemptive)
         throws Exception
     {
-        out.println(format("*** testBasic: version: %s,  async: %s, useSSL: %s, authScheme: %s, authType: %s ***",
-                version, async, useSSL, authScheme, authType));
+        final boolean addHeaders = authScheme == DigestEchoServer.HttpAuthSchemeType.BASICSERVER;
+        // !preemptive has no meaning if we don't handle the authorization
+        // headers ourselves
+        if (!preemptive && !addHeaders) return;
+
+        out.println(format("*** testBasic: version: %s,  async: %s, useSSL: %s, " +
+                        "authScheme: %s, authType: %s, expectContinue: %s preemptive: %s***",
+                version, async, useSSL, authScheme, authType, expectContinue, preemptive));
 
         DigestEchoServer server = EchoServers.of(useSSL ? "https" : "http", authType, authScheme);
         URI uri = DigestEchoServer.uri(useSSL ? "https" : "http", server.getServerAddress(), "/foo/");
@@ -273,7 +290,6 @@
         CompletableFuture<HttpResponse<String>> cf1;
         String auth = null;
 
-
         try {
             for (int i=0; i<data.length; i++) {
                 out.println(DigestEchoServer.now() + " ----- iteration " + i + " -----");
@@ -282,22 +298,26 @@
                 String body = lines.stream().collect(Collectors.joining("\r\n"));
                 HttpRequest.BodyPublisher reqBody = HttpRequest.BodyPublisher.fromString(body);
                 HttpRequest.Builder builder = HttpRequest.newBuilder(uri).version(version)
-                        .POST(reqBody);
-                final boolean addHeaders = authScheme == DigestEchoServer.HttpAuthSchemeType.BASICSERVER;
+                        .POST(reqBody).expectContinue(expectContinue);
+                boolean isTunnel = isProxy(authType) && useSSL;
                 if (addHeaders) {
                     // handle authentication ourselves
                     assert !client.authenticator().isPresent();
                     if (auth == null) auth = "Basic " + getBasicAuth("arthur");
                     try {
-                        builder = builder.header(authorizationKey(authType), auth);
-                        if (isProxy(authType)) {
-                            throw new RuntimeException("Setting " + authorizationKey(authType)
-                                    + " should have failed");
+                        if ((i > 0 || preemptive) && (!isTunnel || i == 0)) {
+                            // In case of a SSL tunnel through proxy then only the
+                            // first request should require proxy authorization
+                            // Though this might be invalidated if the server decides
+                            // to close the connection...
+                            out.println(String.format("%s adding %s: %s",
+                                    DigestEchoServer.now(),
+                                    authorizationKey(authType),
+                                    auth));
+                            builder = builder.header(authorizationKey(authType), auth);
                         }
                     } catch (IllegalArgumentException x) {
-                        if (isProxy(authType)) {
-                            System.out.println("Got expected " + x);
-                        } else throw x;
+                        throw x;
                     }
                 } else {
                     // let the stack do the authentication
@@ -308,9 +328,9 @@
                 HttpResponse<Stream<String>> resp;
                 try {
                     if (async) {
-                        resp = client.sendAsync(request, HttpResponse.BodyHandler.asLines()).join();
+                        resp = client.sendAsync(request, asLines()).join();
                     } else {
-                        resp = client.send(request, HttpResponse.BodyHandler.asLines());
+                        resp = client.send(request, asLines());
                     }
                 } catch (Throwable t) {
                     long stop = System.nanoTime();
@@ -323,25 +343,19 @@
                         assert t.getCause() != null;
                         t = t.getCause();
                     }
-                    // If we let the stack manage the authorisation, or if we
-                    // were not authenticating with a Proxy, then there should
-                    // have been no exception.
-                    if (!addHeaders || !isProxy(authType) || client.authenticator().isPresent()) {
-                        throw new RuntimeException("Unexpected exception: " + t, t);
-                    }
-                    // In the case of Proxy authentication with Basic, we should get
-                    // an IOException complaining that we haven't set an authenticator.
-                    if (t instanceof IOException && t.getMessage().contains("No authenticator set")) {
-                        System.out.println("Got expected exception: " + t);
-                        continue;
-                    }
                     throw new RuntimeException("Unexpected exception: " + t, t);
                 }
-                if (isProxy(authType) && addHeaders) {
-                    assert resp.statusCode() == 407;
-                    continue;
+
+                if (addHeaders && !preemptive && i==0) {
+                    assert resp.statusCode() == 401 || resp.statusCode() == 407;
+                    request = HttpRequest.newBuilder(uri).version(version)
+                            .POST(reqBody).header(authorizationKey(authType), auth).build();
+                    if (async) {
+                        resp = client.sendAsync(request, asLines()).join();
+                    } else {
+                        resp = client.send(request, asLines());
+                    }
                 }
-
                 assert resp.statusCode() == 200;
                 List<String> respLines = resp.body().collect(Collectors.toList());
                 long stop = System.nanoTime();
@@ -370,11 +384,12 @@
     final static AtomicLong digests = new AtomicLong();
     final static AtomicLong digestCount = new AtomicLong();
     // @Test
-    void testDigest(HttpClient.Version version, boolean async)
+    void testDigest(HttpClient.Version version, boolean async, boolean expectContinue)
             throws Exception
     {
-        out.println(format("*** testDigest: version: %s,  async: %s, useSSL: %s, authScheme: %s, authType: %s  ***",
-                version, async, useSSL, authScheme, authType));
+        out.println(format("*** testDigest: version: %s,  async: %s, useSSL: %s, " +
+                        "authScheme: %s, authType: %s, expectContinue: %s  ***",
+                version, async, useSSL, authScheme, authType, expectContinue));
         DigestEchoServer server = EchoServers.of(useSSL ? "https" : "http", authType, authScheme);
 
         URI uri = DigestEchoServer.uri(useSSL ? "https" : "http", server.getServerAddress(), "/foo/");
@@ -393,33 +408,43 @@
                 assert lines.size() == i + 1;
                 String body = lines.stream().collect(Collectors.joining("\r\n"));
                 HttpRequest.BodyPublisher reqBody = HttpRequest.BodyPublisher.fromString(body);
-                HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(uri).version(version).POST(reqBody);
-                if (challenge != null) {
+                HttpRequest.Builder reqBuilder = HttpRequest
+                        .newBuilder(uri).version(version).POST(reqBody)
+                        .expectContinue(expectContinue);
+
+                boolean isTunnel = isProxy(authType) && useSSL;
+                String digestMethod = isTunnel ? "CONNECT" : "POST";
+
+                // In case of a tunnel connection only the first request
+                // which establishes the tunnel needs to authenticate with
+                // the proxy.
+                if (challenge != null && !isTunnel) {
                     assert cnonceStr != null;
-                    String auth = digestResponse(uri, "POST", challenge, cnonceStr);
+                    String auth = digestResponse(uri, digestMethod, challenge, cnonceStr);
                     try {
                         reqBuilder = reqBuilder.header(authorizationKey(authType), auth);
-                        if (isProxy(authType)) {
-                            throw new RuntimeException("Setting " + authorizationKey(authType)
-                                    + " should have failed");
-                        }
                     } catch (IllegalArgumentException x) {
-                        if (isProxy(authType)) {
-                            System.out.println("Got expected " + x);
-                        } else throw x;
+                        throw x;
                     }
                 }
+
                 long start = System.nanoTime();
                 HttpRequest request = reqBuilder.build();
                 HttpResponse<Stream<String>> resp;
                 if (async) {
-                    resp = client.sendAsync(request, HttpResponse.BodyHandler.asLines()).join();
+                    resp = client.sendAsync(request, asLines()).join();
                 } else {
-                    resp = client.send(request, HttpResponse.BodyHandler.asLines());
+                    resp = client.send(request, asLines());
                 }
                 System.out.println(resp);
                 assert challenge != null || resp.statusCode() == 401 || resp.statusCode() == 407;
                 if (resp.statusCode() == 401 || resp.statusCode() == 407) {
+                    // This assert may need to be relaxed if our server happened to
+                    // decide to close the tunnel connection, in which case we would
+                    // receive 407 again...
+                    assert challenge == null || !isTunnel
+                            : "No proxy auth should be required after establishing an SSL tunnel";
+
                     System.out.println("Received " + resp.statusCode() + " answering challenge...");
                     random.nextBytes(cnonce);
                     cnonceStr = new BigInteger(1, cnonce).toString(16);
@@ -434,27 +459,20 @@
                     if (qop == null && nonce == null) {
                         throw new RuntimeException("QOP and NONCE not found");
                     }
-                    challenge =
-                            DigestEchoServer.DigestResponse.create(authenticate.substring("Digest ".length()));
-                    String auth = digestResponse(uri, "POST", challenge, cnonceStr);
+                    challenge = DigestEchoServer.DigestResponse
+                            .create(authenticate.substring("Digest ".length()));
+                    String auth = digestResponse(uri, digestMethod, challenge, cnonceStr);
                     try {
                         request = HttpRequest.newBuilder(uri).version(version)
                             .POST(reqBody).header(authorizationKey(authType), auth).build();
-                        if (isProxy(authType)) {
-                            throw new RuntimeException("Setting " + authorizationKey(authType)
-                                    + " should have failed");
-                        }
                     } catch (IllegalArgumentException x) {
-                        if (isProxy(authType)) {
-                            System.out.println("Got expected " + x);
-                            continue;
-                        } else throw x;
+                        throw x;
                     }
 
                     if (async) {
-                        resp = client.sendAsync(request, HttpResponse.BodyHandler.asLines()).join();
+                        resp = client.sendAsync(request, asLines()).join();
                     } else {
-                        resp = client.send(request, HttpResponse.BodyHandler.asLines());
+                        resp = client.send(request, asLines());
                     }
                     System.out.println(resp);
                 }
--- a/test/jdk/java/net/httpclient/DigestEchoServer.java	Fri Jan 26 11:08:42 2018 +0000
+++ b/test/jdk/java/net/httpclient/DigestEchoServer.java	Fri Jan 26 16:46:52 2018 +0000
@@ -52,6 +52,7 @@
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
@@ -59,12 +60,15 @@
 import java.util.Arrays;
 import java.util.Base64;
 import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Random;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import javax.net.ssl.SSLContext;
 import sun.net.www.HeaderParser;
 
@@ -79,7 +83,10 @@
 
     public static final boolean DEBUG =
             Boolean.parseBoolean(System.getProperty("test.debug", "false"));
-    public enum HttpAuthType { SERVER, PROXY, SERVER307, PROXY305 };
+    public enum HttpAuthType {
+        SERVER, PROXY, SERVER307, PROXY305
+        /* add PROXY_AND_SERVER and SERVER_PROXY_NONE */
+    };
     public enum HttpAuthSchemeType { NONE, BASICSERVER, BASIC, DIGEST };
     public static final HttpAuthType DEFAULT_HTTP_AUTH_TYPE = HttpAuthType.SERVER;
     public static final String DEFAULT_PROTOCOL_TYPE = "https";
@@ -398,7 +405,7 @@
 
         HttpServer impl = createHttpServer(protocol);
         final DigestEchoServer server = new DigestEchoServer(impl, null, delegate);
-        final HttpHandler hh = server.createHandler(schemeType, auth, authType);
+        final HttpHandler hh = server.createHandler(schemeType, auth, authType, false);
         HttpContext ctxt = impl.createContext(path, hh);
         server.configureAuthentication(ctxt, schemeType, auth, authType);
         impl.start();
@@ -419,7 +426,10 @@
         final DigestEchoServer server = "https".equalsIgnoreCase(protocol)
                 ? new HttpsProxyTunnel(impl, null, delegate)
                 : new DigestEchoServer(impl, null, delegate);
-        final HttpHandler hh = server.createHandler(schemeType, auth, authType);
+
+        final HttpHandler hh = server.createHandler(HttpAuthSchemeType.NONE,
+                                    null, HttpAuthType.SERVER,
+                                         server instanceof HttpsProxyTunnel);
         HttpContext ctxt = impl.createContext(path, hh);
         server.configureAuthentication(ctxt, schemeType, auth, authType);
         impl.start();
@@ -492,11 +502,12 @@
 
     private HttpHandler createHandler(HttpAuthSchemeType schemeType,
                                       HttpTestAuthenticator auth,
-                                      HttpAuthType authType) {
-        return new HttpNoAuthHandler(authType);
+                                      HttpAuthType authType,
+                                      boolean tunelled) {
+        return new HttpNoAuthHandler(authType, tunelled);
     }
 
-    private void configureAuthentication(HttpContext ctxt,
+    void configureAuthentication(HttpContext ctxt,
                             HttpAuthSchemeType schemeType,
                             HttpTestAuthenticator auth,
                             HttpAuthType authType) {
@@ -994,13 +1005,35 @@
 
     private class HttpNoAuthHandler extends AbstractHttpHandler {
 
-        public HttpNoAuthHandler(HttpAuthType authType) {
+        // true if this server is behind a proxy tunnel.
+        final boolean tunnelled;
+        public HttpNoAuthHandler(HttpAuthType authType, boolean tunnelled) {
             super(authType, authType == HttpAuthType.SERVER
                             ? "NoAuth Server" : "NoAuth Proxy");
+            this.tunnelled = tunnelled;
         }
 
         @Override
         protected void sendResponse(HttpExchange he) throws IOException {
+            if (DEBUG) {
+                System.out.println(type + ": headers are: "
+                        + DigestEchoServer.toString(he.getRequestHeaders()));
+            }
+            if (authType == HttpAuthType.SERVER && tunnelled) {
+                // Verify that the client doesn't send us proxy-* headers
+                // used to establish the proxy tunnel
+                Optional<String> proxyAuth = he.getRequestHeaders()
+                        .keySet().stream()
+                        .filter("proxy-authorization"::equalsIgnoreCase)
+                        .findAny();
+                if (proxyAuth.isPresent()) {
+                    System.out.println(type + " found "
+                            + proxyAuth.get() + ": failing!");
+                    throw new IOException(proxyAuth.get()
+                            + " found by " + type + " for "
+                            + he.getRequestURI());
+                }
+            }
             DigestEchoServer.this.writeResponse(he);
         }
 
@@ -1057,6 +1090,172 @@
         long nan = now % 1000_000;
         return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan);
     }
+
+    static class  ProxyAuthorization {
+        final HttpAuthSchemeType schemeType;
+        final HttpTestAuthenticator authenticator;
+        private final byte[] nonce;
+        private final String ns;
+
+        ProxyAuthorization(HttpAuthSchemeType schemeType, HttpTestAuthenticator auth) {
+            this.schemeType = schemeType;
+            this.authenticator = auth;
+            nonce = new byte[16];
+            new Random(Instant.now().toEpochMilli()).nextBytes(nonce);
+            ns = new BigInteger(1, nonce).toString(16);
+        }
+
+        String doBasic(Optional<String> authorization) {
+            String offset = "proxy-authorization: basic ";
+            String authstring = authorization.orElse("");
+            if (!authstring.toLowerCase(Locale.US).startsWith(offset)) {
+                return "Proxy-Authenticate: BASIC " + "realm=\""
+                        + authenticator.getRealm() +"\"";
+            }
+            authstring = authstring
+                    .substring(offset.length())
+                    .trim();
+            byte[] base64 = Base64.getDecoder().decode(authstring);
+            String up = new String(base64, StandardCharsets.UTF_8);
+            int colon = up.indexOf(':');
+            if (colon < 1) {
+                return "Proxy-Authenticate: BASIC " + "realm=\""
+                        + authenticator.getRealm() +"\"";
+            }
+            String u = up.substring(0, colon);
+            String p = up.substring(colon+1);
+            char[] pw = authenticator.getPassword(u);
+            if (!p.equals(new String(pw))) {
+                return "Proxy-Authenticate: BASIC " + "realm=\""
+                        + authenticator.getRealm() +"\"";
+            }
+            System.out.println(now() + " Proxy basic authentication success");
+            return null;
+        }
+
+        String doDigest(Optional<String> authorization) {
+            String offset = "proxy-authorization: digest ";
+            String authstring = authorization.orElse("");
+            if (!authstring.toLowerCase(Locale.US).startsWith(offset)) {
+                return "Proxy-Authenticate: " +
+                        "Digest realm=\"" + authenticator.getRealm() + "\","
+                        + "\r\n    qop=\"auth\","
+                        + "\r\n    nonce=\"" + ns +"\"";
+            }
+            authstring = authstring
+                    .substring(offset.length())
+                    .trim();
+            boolean validated = false;
+            try {
+                DigestResponse dgr = DigestResponse.create(authstring);
+                validated = validate("CONNECT", dgr);
+            } catch (Throwable t) {
+                t.printStackTrace();
+            }
+            if (!validated) {
+                return "Proxy-Authenticate: " +
+                        "Digest realm=\"" + authenticator.getRealm() + "\","
+                        + "\r\n    qop=\"auth\","
+                        + "\r\n    nonce=\"" + ns +"\"";
+            }
+            return null;
+        }
+
+
+
+
+        boolean validate(String reqMethod, DigestResponse dg) {
+            String type = now() + this.getClass().getSimpleName();
+            if (!"MD5".equalsIgnoreCase(dg.getAlgorithm("MD5"))) {
+                System.out.println(type + ": Unsupported algorithm "
+                        + dg.algorithm);
+                return false;
+            }
+            if (!"auth".equalsIgnoreCase(dg.getQoP("auth"))) {
+                System.out.println(type + ": Unsupported qop "
+                        + dg.qop);
+                return false;
+            }
+            try {
+                if (!dg.nonce.equals(ns)) {
+                    System.out.println(type + ": bad nonce returned by client: "
+                            + nonce + " expected " + ns);
+                    return false;
+                }
+                if (dg.response == null) {
+                    System.out.println(type + ": missing digest response.");
+                    return false;
+                }
+                char[] pa = authenticator.getPassword(dg.username);
+                return verify(type, reqMethod, dg, pa);
+            } catch(IllegalArgumentException | SecurityException
+                    | NoSuchAlgorithmException e) {
+                System.out.println(type + ": " + e.getMessage());
+                return false;
+            }
+        }
+
+
+        boolean verify(String type, String reqMethod, DigestResponse dg, char[] pw)
+                throws NoSuchAlgorithmException {
+            String response = DigestResponse.computeDigest(true, reqMethod, pw, dg);
+            if (!dg.response.equals(response)) {
+                System.out.println(type + ": bad response returned by client: "
+                        + dg.response + " expected " + response);
+                return false;
+            } else {
+                // A real server would also verify the uri=<request-uri>
+                // parameter - but this is just a test...
+                System.out.println(type + ": verified response " + response);
+            }
+            return true;
+        }
+
+        public boolean authorize(StringBuilder response, String requestLine, String headers) {
+            String message = "<html><body><p>Authorization Failed%s</p></body></html>\r\n";
+            if (authenticator == null && schemeType != HttpAuthSchemeType.NONE) {
+                message = String.format(message, " No Authenticator Set");
+                response.append("HTTP/1.1 407 Proxy Authentication Failed\r\n");
+                response.append("Content-Length: ")
+                        .append(message.getBytes(StandardCharsets.UTF_8).length)
+                        .append("\r\n\r\n");
+                response.append(message);
+                return false;
+            }
+            Optional<String> authorization = Stream.of(headers.split("\r\n"))
+                    .filter((k) -> k.toLowerCase(Locale.US).startsWith("proxy-authorization:"))
+                    .findFirst();
+            String authenticate = null;
+            switch(schemeType) {
+                case BASIC:
+                case BASICSERVER:
+                    authenticate = doBasic(authorization);
+                    break;
+                case DIGEST:
+                    authenticate = doDigest(authorization);
+                    break;
+                case NONE:
+                    response.append("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
+                    return true;
+                default:
+                    throw new InternalError("Unknown scheme type: " + schemeType);
+            }
+            if (authenticate != null) {
+                message = String.format(message, "");
+                response.append("HTTP/1.1 407 Proxy Authentication Required\r\n");
+                response.append("Content-Length: ")
+                        .append(message.getBytes(StandardCharsets.UTF_8).length)
+                        .append("\r\n")
+                        .append(authenticate)
+                        .append("\r\n\r\n");
+                response.append(message);
+                return false;
+            }
+            response.append("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
+            return true;
+        }
+    }
+
     // This is a bit hacky: HttpsProxyTunnel is an HTTPTestServer hidden
     // behind a fake proxy that only understands CONNECT requests.
     // The fake proxy is just a server socket that intercept the
@@ -1067,6 +1266,7 @@
         final ServerSocket ss;
         final CopyOnWriteArrayList<CompletableFuture<Void>> connectionCFs
                 = new CopyOnWriteArrayList<>();
+        volatile ProxyAuthorization authorization;
         volatile boolean stopped;
         public HttpsProxyTunnel(HttpServer server, DigestEchoServer target,
                                HttpHandler delegate)
@@ -1095,6 +1295,27 @@
             }
         }
 
+
+        @Override
+        void configureAuthentication(HttpContext ctxt,
+                                     HttpAuthSchemeType schemeType,
+                                     HttpTestAuthenticator auth,
+                                     HttpAuthType authType) {
+            if (authType == HttpAuthType.PROXY || authType == HttpAuthType.PROXY305) {
+                authorization = new ProxyAuthorization(schemeType, auth);
+            } else {
+                super.configureAuthentication(ctxt, schemeType, auth, authType);
+            }
+        }
+
+        boolean authorize(StringBuilder response, String requestLine, String headers) {
+            if (authorization != null) {
+                return authorization.authorize(response, requestLine, headers);
+            }
+            response.append("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
+            return true;
+        }
+
         // Pipe the input stream to the output stream.
         private synchronized Thread pipe(InputStream is, OutputStream os, char tag, CompletableFuture<Void> end) {
             return new Thread("TunnelPipe("+tag+")") {
@@ -1168,6 +1389,7 @@
                         break;
                     }
                     System.out.println(now() + "Tunnel: Client accepted");
+                    StringBuilder headers = new StringBuilder();
                     Socket targetConnection = null;
                     InputStream  ccis = clientConnection.getInputStream();
                     OutputStream ccos = clientConnection.getOutputStream();
@@ -1184,19 +1406,32 @@
 
                         // Read all headers until we find the empty line that
                         // signals the end of all headers.
-                        while(!requestLine.equals("")) {
+                        String line = requestLine;
+                        while(!line.equals("")) {
                             System.out.println(now() + "Tunnel: Reading header: "
-                                               + (requestLine = readLine(ccis)));
+                                               + (line = readLine(ccis)));
+                            headers.append(line).append("\r\n");
                         }
 
+                        StringBuilder response = new StringBuilder();
+                        final boolean authorize = authorize(response, requestLine, headers.toString());
+                        if (!authorize) {
+                            System.out.println(now() + "Tunnel: Sending "
+                                    + response);
+                            // send the 407 response
+                            pw.print(response.toString());
+                            pw.flush();
+                            toClose.close();
+                            continue;
+                        }
                         targetConnection = new Socket(
                                 serverImpl.getAddress().getAddress(),
                                 serverImpl.getAddress().getPort());
 
                         // Then send the 200 OK response to the client
                         System.out.println(now() + "Tunnel: Sending "
-                                           + "HTTP/1.1 200 OK\r\n\r\n");
-                        pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
+                                           + response);
+                        pw.print(response);
                         pw.flush();
                     } else {
                         // This should not happen. If it does let our serverImpl
--- a/test/jdk/java/net/httpclient/MultiAuthTest.java	Fri Jan 26 11:08:42 2018 +0000
+++ b/test/jdk/java/net/httpclient/MultiAuthTest.java	Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 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
@@ -176,6 +176,9 @@
             resp = client.send(req, asString());
             ok = resp.statusCode() == 200 &&
                 resp.body().equals(RESPONSE);
+            if (resp.statusCode() == 401 || resp.statusCode() == 407) {
+                throw new IOException(String.valueOf(resp));
+            }
             if (expectFailure != null) {
                 throw new RuntimeException("Expected " + expectFailure.getName()
                          +" not raised");
--- a/test/jdk/java/net/httpclient/RequestBuilderTest.java	Fri Jan 26 11:08:42 2018 +0000
+++ b/test/jdk/java/net/httpclient/RequestBuilderTest.java	Fri Jan 26 16:46:52 2018 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 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
@@ -315,15 +315,12 @@
     private static final Set<String> RESTRICTED = Set.of("connection", "content-length",
             "date", "expect", "from", "host", "origin",
             "referer", "upgrade", "via", "warning",
-            "proxy-authorization",
             "Connection", "Content-Length",
             "DATE", "eXpect", "frOm", "hosT", "origIN",
             "ReFerer", "upgradE", "vIa", "Warning",
-            "Proxy-Authorization",
             "CONNection", "CONTENT-LENGTH",
             "Date", "EXPECT", "From", "Host", "Origin",
-            "Referer", "Upgrade", "Via", "WARNING",
-            "PROXY-AUTHORIZATION");
+            "Referer", "Upgrade", "Via", "WARNING");
 
     interface WithHeader {
         HttpRequest.Builder withHeader(HttpRequest.Builder builder, String name, String value);