http-client-branch: WebSocket permission checks, test updates, and more http-client-branch
authorchegar
Sun, 05 Nov 2017 21:19:55 +0000
branchhttp-client-branch
changeset 55764 34d7cc00f87a
parent 55763 634d8e14c172
child 55765 c56b15998860
http-client-branch: WebSocket permission checks, test updates, and more
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/Http1Request.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2ClientImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2Connection.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClient.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequest.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestBuilderImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponse.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/MultiExchange.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/WebSocket.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/BuilderImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/OpeningHandshake.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/WebSocketImpl.java
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/WebSocketRequest.java
test/jdk/java/net/httpclient/ProxyAuthTest.java
test/jdk/java/net/httpclient/ProxyServer.java
test/jdk/java/net/httpclient/ProxyTest.java
test/jdk/java/net/httpclient/RequestBuilderTest.java
test/jdk/java/net/httpclient/SmokeTest.java
test/jdk/java/net/httpclient/security/Driver.java
test/jdk/java/net/httpclient/security/Security.java
test/jdk/java/net/httpclient/websocket/ConnectionHandover.java
test/jdk/java/net/httpclient/websocket/DummyWebSocketServer.java
test/jdk/java/net/httpclient/websocket/jdk.incubator.httpclient/jdk/incubator/http/internal/websocket/BuildingWebSocketTest.java
test/jdk/java/net/httpclient/websocket/security/WSURLPermissionTest.java
test/jdk/java/net/httpclient/websocket/security/httpclient.policy
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AuthenticationFilter.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AuthenticationFilter.java	Sun Nov 05 21:19:55 2017 +0000
@@ -83,7 +83,7 @@
     }
 
     private URI getProxyURI(HttpRequestImpl r) {
-        InetSocketAddress proxy = r.proxy(exchange.client());
+        InetSocketAddress proxy = r.proxy();
         if (proxy == null) {
             return null;
         }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Exchange.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Exchange.java	Sun Nov 05 21:19:55 2017 +0000
@@ -28,22 +28,17 @@
 import java.io.IOException;
 import java.lang.System.Logger.Level;
 import java.net.InetSocketAddress;
+import java.net.Proxy;
 import java.net.ProxySelector;
 import java.net.SocketPermission;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URLPermission;
 import java.security.AccessControlContext;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
-import java.security.PrivilegedActionException;
-import java.security.PrivilegedExceptionAction;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionException;
 import java.util.concurrent.Executor;
 import jdk.incubator.http.internal.common.MinimalFuture;
 import jdk.incubator.http.internal.common.Utils;
@@ -425,9 +420,9 @@
      * Returns the security permission required for the given details.
      * If method is CONNECT, then uri must be of form "scheme://host:port"
      */
-    private static URLPermission getPermissionFor(URI uri,
-                                                  String method,
-                                                  Map<String, List<String>> headers) {
+    private static URLPermission permissionForServer(URI uri,
+                                                    String method,
+                                                    Map<String, List<String>> headers) {
         StringBuilder sb = new StringBuilder();
 
         String urlstring, actionstring;
@@ -461,20 +456,39 @@
     }
 
     /**
+     * Returns the security permissions required to connect to the proxy, or
+     * null if none is required or applicable.
+     */
+    static URLPermission permissionForProxy(HttpRequestImpl request) {
+        InetSocketAddress proxyAddress = request.proxy();
+        if (proxyAddress == null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        sb.append("socket://")
+          .append(proxyAddress.getHostString()).append(":")
+          .append(proxyAddress.getPort());
+        String urlstring = sb.toString();
+        return new URLPermission(urlstring.toString(), "CONNECT");
+    }
+
+    /**
      * Performs the necessary security permission checks required to retrieve
      * the response. Returns a security exception representing the denied
      * permission, or null if all checks pass or there is no security manager.
      */
     private SecurityException checkPermissions() {
+        String method = request.method();
         SecurityManager sm = System.getSecurityManager();
-        if (sm == null) {
+        if (sm == null || method.equals("CONNECT")) {
+            // tunneling will have a null acc, which is fine. The proxy
+            // permission check will have already been preformed.
             return null;
         }
 
-        String method = request.method();
         HttpHeaders userHeaders = request.getUserHeaders();
         URI u = getURIForSecurityCheck();
-        URLPermission p = getPermissionFor(u, method, userHeaders.map());
+        URLPermission p = permissionForServer(u, method, userHeaders.map());
 
         try {
             assert acc != null;
@@ -484,22 +498,15 @@
         }
         ProxySelector ps = client.proxy().orElse(null);
         if (ps != null) {
-            InetSocketAddress proxy = (InetSocketAddress)
-                    ps.select(u).get(0).address(); // TODO: check this
-            // may need additional check
             if (!method.equals("CONNECT")) {
-                // a direct http proxy. Need to check access to proxy
-                try {
-                    u = new URI("socket", null, proxy.getHostString(),
-                        proxy.getPort(), null, null, null);
-                } catch (URISyntaxException e) {
-                    throw new InternalError(e); // shouldn't happen
-                }
-                p = new URLPermission(u.toString(), "CONNECT");
-                try {
-                    sm.checkPermission(p, acc);
-                } catch (SecurityException e) {
-                    return e;
+                // a non-tunneling HTTP proxy. Need to check access
+                URLPermission proxyPerm = permissionForProxy(request);
+                if (proxyPerm != null) {
+                    try {
+                        sm.checkPermission(proxyPerm, acc);
+                    } catch (SecurityException e) {
+                        return e;
+                    }
                 }
             }
         }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Request.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Request.java	Sun Nov 05 21:19:55 2017 +0000
@@ -145,7 +145,7 @@
         URI uri = request.uri();
         String method = request.method();
 
-        if ((request.proxy(client) == null && !method.equals("CONNECT"))
+        if ((request.proxy() == null && !method.equals("CONNECT"))
                 || request.isWebSocket()) {
             return getPathAndQuery(uri);
         }
@@ -158,6 +158,11 @@
                 return getPathAndQuery(uri);
             }
         }
+        if (request.method().equals("CONNECT")) {
+            // use authority for connect itself
+            return authorityString(request.authority());
+        }
+
         return uri == null? authorityString(request.authority()) : uri.toString();
     }
 
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2ClientImpl.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2ClientImpl.java	Sun Nov 05 21:19:55 2017 +0000
@@ -98,7 +98,7 @@
      */
     CompletableFuture<Http2Connection> getConnectionFor(HttpRequestImpl req) {
         URI uri = req.uri();
-        InetSocketAddress proxy = req.proxy(client);
+        InetSocketAddress proxy = req.proxy();
         String key = Http2Connection.keyFor(uri, proxy);
 
         synchronized (opening) {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2Connection.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2Connection.java	Sun Nov 05 21:19:55 2017 +0000
@@ -320,7 +320,7 @@
         this(connection,
              h2client,
              1,
-             keyFor(request.uri(), request.proxy(h2client.client())));
+             keyFor(request.uri(), request.proxy()));
 
         Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
 
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClient.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClient.java	Sun Nov 05 21:19:55 2017 +0000
@@ -372,7 +372,8 @@
      * @param responseBodyHandler the response body handler
      * @return the response body
      * @throws java.io.IOException if an I/O error occurs when sending or receiving
-     * @throws java.lang.InterruptedException if the operation is interrupted
+     * @throws InterruptedException if the operation is interrupted
+     * @throws IllegalArgumentException if the request method is not supported
      * @throws SecurityException If a security manager has been installed
      *          and it denies {@link java.net.URLPermission access} to the
      *          URL in the given request, or proxy if one is configured.
@@ -459,6 +460,7 @@
      * @return a builder of {@code WebSocket} instances
      * @throws UnsupportedOperationException
      *         if this {@code HttpClient} does not provide WebSocket support
+     * @throws IllegalArgumentException if the given URI is not a valid WebSocket URI
      */
     public WebSocket.Builder newWebSocketBuilder(URI uri,
                                                  WebSocket.Listener listener)
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientImpl.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientImpl.java	Sun Nov 05 21:19:55 2017 +0000
@@ -33,6 +33,7 @@
 import java.net.Authenticator;
 import java.net.CookieManager;
 import java.net.NetPermission;
+import java.net.Proxy;
 import java.net.ProxySelector;
 import java.net.URI;
 import java.nio.channels.CancelledKeyException;
@@ -407,23 +408,32 @@
 
     @Override
     public <T> CompletableFuture<HttpResponse<T>>
-    sendAsync(HttpRequest req, BodyHandler<T> responseHandler)
+    sendAsync(HttpRequest userRequest, BodyHandler<T> responseHandler)
     {
         AccessControlContext acc = null;
         if (System.getSecurityManager() != null)
             acc = AccessController.getContext();
 
+        // Clone the, possibly untrusted, HttpRequest
+        HttpRequestImpl requestImpl = new HttpRequestImpl(userRequest, proxySelector, acc);
+        if (requestImpl.method().equals("CONNECT"))
+            throw new IllegalArgumentException("Unsupported method CONNECT");
+
         long start = DEBUGELAPSED ? System.nanoTime() : 0;
         reference();
         try {
-            debug.log(Level.DEBUG, "ClientImpl (async) send %s", req);
+            debug.log(Level.DEBUG, "ClientImpl (async) send %s", userRequest);
 
-            MultiExchange<Void,T> mex = new MultiExchange<>(req, this, responseHandler, acc);
+            MultiExchange<Void,T> mex = new MultiExchange<>(userRequest,
+                                                            requestImpl,
+                                                            this,
+                                                            responseHandler,
+                                                            acc);
             CompletableFuture<HttpResponse<T>> res =
                     mex.responseAsync().whenComplete((b,t) -> unreference());
             if (DEBUGELAPSED) {
                 res = res.whenComplete(
-                        (b,t) -> debugCompleted("ClientImpl (async)", start, req));
+                        (b,t) -> debugCompleted("ClientImpl (async)", start, userRequest));
             }
             // makes sure that any dependent actions happen in the executor
             if (acc != null) {
@@ -434,29 +444,38 @@
             return res;
         } catch(Throwable t) {
             unreference();
-            debugCompleted("ClientImpl (async)", start, req);
+            debugCompleted("ClientImpl (async)", start, userRequest);
             throw t;
         }
     }
 
     @Override
     public <U, T> CompletableFuture<U>
-    sendAsync(HttpRequest req, MultiSubscriber<U, T> responseHandler) {
+    sendAsync(HttpRequest userRequest, MultiSubscriber<U, T> responseHandler) {
         AccessControlContext acc = null;
         if (System.getSecurityManager() != null)
             acc = AccessController.getContext();
 
+        // Clone the, possibly untrusted, HttpRequest
+        HttpRequestImpl requestImpl = new HttpRequestImpl(userRequest, proxySelector, acc);
+        if (requestImpl.method().equals("CONNECT"))
+            throw new IllegalArgumentException("Unsupported method CONNECT");
+
         long start = DEBUGELAPSED ? System.nanoTime() : 0;
         reference();
         try {
-            debug.log(Level.DEBUG, "ClientImpl (async) send multi %s", req);
+            debug.log(Level.DEBUG, "ClientImpl (async) send multi %s", userRequest);
 
-            MultiExchange<U,T> mex = new MultiExchange<>(req, this, responseHandler, acc);
+            MultiExchange<U,T> mex = new MultiExchange<>(userRequest,
+                                                         requestImpl,
+                                                         this,
+                                                         responseHandler,
+                                                         acc);
             CompletableFuture<U> res = mex.multiResponseAsync()
                       .whenComplete((b,t) -> unreference());
             if (DEBUGELAPSED) {
                 res = res.whenComplete(
-                        (b,t) -> debugCompleted("ClientImpl (async)", start, req));
+                        (b,t) -> debugCompleted("ClientImpl (async)", start, userRequest));
             }
             // makes sure that any dependent actions happen in the executor
             if (acc != null) {
@@ -467,7 +486,7 @@
             return res;
         } catch(Throwable t) {
             unreference();
-            debugCompleted("ClientImpl (async)", start, req);
+            debugCompleted("ClientImpl (async)", start, userRequest);
             throw t;
         }
     }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java	Sun Nov 05 21:19:55 2017 +0000
@@ -153,7 +153,7 @@
                                                HttpRequestImpl request,
                                                Version version) {
         HttpConnection c = null;
-        InetSocketAddress proxy = request.proxy(client);
+        InetSocketAddress proxy = request.proxy();
         if (proxy != null && proxy.isUnresolved()) {
             // The default proxy selector may select a proxy whose  address is
             // unresolved. We must resolve the address before connecting to it.
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequest.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequest.java	Sun Nov 05 21:19:55 2017 +0000
@@ -199,10 +199,10 @@
  * default executor will execute asynchronous and dependent tasks in a context
  * that is granted no permissions. Custom {@linkplain HttpRequest.BodyPublisher
  * request body publishers}, {@linkplain HttpResponse.BodyHandler response body
- * handlers}, and {@linkplain HttpResponse.BodySubscriber response body
- * subscribers}, if executing operations that require privileges, should do so
- * within an appropriate {@linkplain AccessController#doPrivileged(PrivilegedAction)
- * privileged context}.
+ * handlers}, {@linkplain HttpResponse.BodySubscriber response body subscribers},
+ * and {@linkplain WebSocket.Listener WebSocket Listeners}, if executing
+ * operations that require privileges, should do so  within an appropriate
+ * {@linkplain AccessController#doPrivileged(PrivilegedAction) privileged context}.
  *
  * <p> <b>Examples</b>
  * <pre>{@code
@@ -437,8 +437,8 @@
          * @apiNote The {@linkplain #noBody() noBody} request body publisher can
          * be used where no request body is required or appropriate.
          *
+         * @param method the method to use
          * @param bodyPublisher the body publisher
-         * @param method the method to use
          * @return a {@code HttpRequest}
          * @throws IllegalArgumentException if the method is unrecognised
          */
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestBuilderImpl.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestBuilderImpl.java	Sun Nov 05 21:19:55 2017 +0000
@@ -187,6 +187,8 @@
         requireNonNull(method);
         if (method.equals(""))
             throw newIAE("illegal method <empty string>");
+        if (method.equals("CONNECT"))
+            throw newIAE("method CONNECT is not supported");
         return method0(method, requireNonNull(body));
     }
 
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java	Sun Nov 05 21:19:55 2017 +0000
@@ -30,12 +30,14 @@
 
 import java.io.IOException;
 import java.net.InetSocketAddress;
+import java.net.Proxy;
 import java.net.ProxySelector;
 import java.net.URI;
 import java.security.AccessControlContext;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.time.Duration;
+import java.util.List;
 import java.util.Locale;
 import java.util.Optional;
 
@@ -46,6 +48,7 @@
     private final HttpHeaders userHeaders;
     private final HttpHeadersImpl systemHeaders;
     private final URI uri;
+    private Proxy proxy;
     private InetSocketAddress authority; // only used when URI not specified
     private final String method;
     final BodyPublisher requestPublisher;
@@ -66,6 +69,7 @@
         this.systemHeaders = new HttpHeadersImpl();
         this.uri = builder.uri();
         assert uri != null;
+        this.proxy = null;
         this.expectContinue = builder.expectContinue();
         this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https");
         this.requestPublisher = builder.bodyPublisher();  // may be null
@@ -76,7 +80,7 @@
     /**
      * Creates an HttpRequestImpl from the given request.
      */
-    public HttpRequestImpl(HttpRequest request, AccessControlContext acc) {
+    public HttpRequestImpl(HttpRequest request, ProxySelector ps, AccessControlContext acc) {
         String method = request.method();
         this.method = method == null ? "GET" : method;
         this.userHeaders = request.headers();
@@ -87,6 +91,15 @@
             this.systemHeaders = new HttpHeadersImpl();
         }
         this.uri = request.uri();
+        if (isWebSocket) {
+            // WebSocket determines and sets the proxy itself
+            this.proxy = ((HttpRequestImpl) request).proxy;
+        } else {
+            if (ps != null)
+                this.proxy = retrieveProxy(ps, uri);
+            else
+                this.proxy = null;
+        }
         this.expectContinue = request.expectContinue();
         this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https");
         this.requestPublisher = request.bodyPublisher().orElse(null);
@@ -107,6 +120,7 @@
         this.isWebSocket = other.isWebSocket;
         this.systemHeaders = other.systemHeaders;
         this.uri = uri;
+        this.proxy = other.proxy;
         this.expectContinue = other.expectContinue;
         this.secure = uri.getScheme().toLowerCase(Locale.US).equals("https");
         this.requestPublisher = other.requestPublisher;  // may be null
@@ -125,6 +139,7 @@
         this.userHeaders = ImmutableHeaders.empty();
         this.uri = URI.create("socket://" + authority.getHostString() + ":"
                               + Integer.toString(authority.getPort()) + "/");
+        this.proxy = null;
         this.requestPublisher = null;
         this.authority = authority;
         this.secure = false;
@@ -166,7 +181,7 @@
         StringBuilder sb = new StringBuilder();
         sb.append(scheme).append("://").append(authority).append(path);
         this.uri = URI.create(sb.toString());
-
+        this.proxy = null;
         this.userHeaders = ImmutableHeaders.of(headers.map(), ALLOWED_HEADERS);
         this.systemHeaders = parent.systemHeaders;
         this.expectContinue = parent.expectContinue;
@@ -198,17 +213,35 @@
     @Override
     public boolean expectContinue() { return expectContinue; }
 
-    InetSocketAddress proxy(HttpClientImpl client) {
-        ProxySelector ps = client.proxy().orElse(null);
-        if (ps == null || method.equalsIgnoreCase("CONNECT")) {
+    /** Retrieves the proxy, from the given ProxySelector, if there is one. */
+    private static Proxy retrieveProxy(ProxySelector ps, URI uri) {
+        Proxy proxy = null;
+        List<Proxy> pl = ps.select(uri);
+        if (pl.size() > 0) {
+            Proxy p = pl.get(0);
+            if (p.type().equals(Proxy.Type.HTTP))
+                proxy = p;
+        }
+        return proxy;
+    }
+
+    InetSocketAddress proxy() {
+        if (proxy == null || !proxy.type().equals(Proxy.Type.HTTP)
+                || method.equalsIgnoreCase("CONNECT")) {
             return null;
         }
-        return (InetSocketAddress)ps.select(uri).get(0).address();
+        return (InetSocketAddress)proxy.address();
     }
 
     boolean secure() { return secure; }
 
     @Override
+    public void setProxy(Proxy proxy) {
+        assert isWebSocket;
+        this.proxy = proxy;
+    }
+
+    @Override
     public void isWebSocket(boolean is) {
         isWebSocket = is;
     }
@@ -269,7 +302,7 @@
         }
         final String host = uri.getHost();
         final int port = p;
-        if (proxy(client) == null) {
+        if (proxy() == null) {
             PrivilegedAction<InetSocketAddress> pa = () -> new InetSocketAddress(host, port);
             return AccessController.doPrivileged(pa);
         } else {
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponse.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponse.java	Sun Nov 05 21:19:55 2017 +0000
@@ -803,7 +803,7 @@
          * of {@link #completion(CompletableFuture, CompletableFuture)} can be used to determine
          * when the final PUSH_PROMISE is received.
          *
-         * @param request the push promise
+         * @param pushPromise the push promise
          *
          * @return an optional body handler
          */
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/MultiExchange.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/MultiExchange.java	Sun Nov 05 21:19:55 2017 +0000
@@ -98,13 +98,14 @@
     /**
      * MultiExchange with one final response.
      */
-    MultiExchange(HttpRequest req,
+    MultiExchange(HttpRequest userRequest,
+                  HttpRequestImpl requestImpl,
                   HttpClientImpl client,
                   HttpResponse.BodyHandler<T> responseHandler,
                   AccessControlContext acc) {
         this.previous = null;
-        this.userRequest = req;
-        this.request = new HttpRequestImpl(req, acc);
+        this.userRequest = userRequest;
+        this.request = requestImpl;
         this.currentreq = request;
         this.client = client;
         this.filters = client.filterChain();
@@ -124,13 +125,14 @@
     /**
      * MultiExchange with multiple responses (HTTP/2 server pushes).
      */
-    MultiExchange(HttpRequest req,
+    MultiExchange(HttpRequest userRequest,
+                  HttpRequestImpl requestImpl,
                   HttpClientImpl client,
                   HttpResponse.MultiSubscriber<U, T> multiResponseSubscriber,
                   AccessControlContext acc) {
         this.previous = null;
-        this.userRequest = req;
-        this.request = new HttpRequestImpl(req, acc);
+        this.userRequest = userRequest;
+        this.request = requestImpl;
         this.currentreq = request;
         this.client = client;
         this.filters = client.filterChain();
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java	Sun Nov 05 21:19:55 2017 +0000
@@ -64,7 +64,7 @@
                 HttpClientImpl client = client();
                 assert client != null;
                 HttpRequestImpl req = new HttpRequestImpl("CONNECT", address);
-                MultiExchange<Void,Void> mulEx = new MultiExchange<>(req, client, discard(null), null);
+                MultiExchange<Void,Void> mulEx = new MultiExchange<>(null, req, client, discard(null), null);
                 Exchange<Void> connectExchange = new Exchange<>(req, mulEx);
 
                 return connectExchange
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/WebSocket.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/WebSocket.java	Sun Nov 05 21:19:55 2017 +0000
@@ -192,8 +192,12 @@
          * <li> {@link InterruptedException} -
          *          if the operation was interrupted
          * <li> {@link SecurityException} -
-         *          if a security manager is set, and the caller does not
-         *          have a {@link java.net.URLPermission} for the WebSocket URI
+         *          If a security manager has been installed and it denies
+         *          {@link java.net.URLPermission access} to the WebSocket URI,
+         *          that was used to create this builder.
+         *          <a href="HttpRequest.html#securitychecks">Security checks</a>
+         *          contains more information relating to the security context
+         *          in which the the listener is invoked.
          * <li> {@link IllegalArgumentException} -
          *          if any of the additional {@link #header(String, String)
          *          headers} are illegal;
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/BuilderImpl.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/BuilderImpl.java	Sun Nov 05 21:19:55 2017 +0000
@@ -38,6 +38,7 @@
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 
+import static java.lang.String.format;
 import static java.util.Objects.requireNonNull;
 import static jdk.incubator.http.internal.common.Pair.pair;
 
@@ -52,10 +53,28 @@
 
     public BuilderImpl(HttpClient client, URI uri, Listener listener) {
         this.client = requireNonNull(client, "client");
-        this.uri = requireNonNull(uri, "uri");
+        this.uri = checkURI(requireNonNull(uri, "uri"));
         this.listener = requireNonNull(listener, "listener");
     }
 
+    private static IllegalArgumentException newIAE(String message, Object... args) {
+        return new IllegalArgumentException(format(message, args));
+    }
+
+    private static URI checkURI(URI uri) {
+        String scheme = uri.getScheme();
+        if (scheme == null)
+            throw newIAE("URI with undefined scheme");
+        scheme = scheme.toLowerCase();
+        if (!(scheme.equals("ws") || scheme.equals("wss")))
+            throw newIAE("invalid URI scheme %s", scheme);
+        if (uri.getHost() == null)
+            throw newIAE("URI must contain a host: %s", uri);
+        if (uri.getFragment() != null)
+            throw newIAE("URI must not contain a fragment: %s", uri);
+        return uri;
+    }
+
     @Override
     public Builder header(String name, String value) {
         requireNonNull(name, "name");
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/OpeningHandshake.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/OpeningHandshake.java	Sun Nov 05 21:19:55 2017 +0000
@@ -28,6 +28,7 @@
 import jdk.incubator.http.internal.common.MinimalFuture;
 
 import java.io.IOException;
+import java.net.Proxy;
 import java.net.URI;
 import java.net.URISyntaxException;
 import jdk.incubator.http.HttpClient;
@@ -40,8 +41,10 @@
 import jdk.incubator.http.internal.common.Pair;
 
 import java.nio.charset.StandardCharsets;
+import java.security.AccessController;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.security.PrivilegedAction;
 import java.security.SecureRandom;
 import java.time.Duration;
 import java.util.Base64;
@@ -99,7 +102,7 @@
     private final Collection<String> subprotocols;
     private final String nonce;
 
-    OpeningHandshake(BuilderImpl b) {
+    OpeningHandshake(BuilderImpl b, Proxy proxy) {
         this.client = b.getClient();
         URI httpURI = createRequestURI(b.getUri());
         HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(httpURI);
@@ -130,6 +133,7 @@
         r.isWebSocket(true);
         r.setSystemHeader(HEADER_UPGRADE, "websocket");
         r.setSystemHeader(HEADER_CONNECTION, "Upgrade");
+        r.setProxy(proxy);
     }
 
     private static Collection<String> createRequestSubprotocols(
@@ -153,9 +157,7 @@
      *
      * https://tools.ietf.org/html/rfc6455#section-3
      */
-    private static URI createRequestURI(URI uri) {
-        // TODO: check permission for WebSocket URI and translate it into
-        // http/https permission
+    static URI createRequestURI(URI uri) {
         String s = uri.getScheme(); // The scheme might be null (i.e. undefined)
         if (!("ws".equalsIgnoreCase(s) || "wss".equalsIgnoreCase(s))
                 || uri.getFragment() != null)
@@ -178,8 +180,10 @@
     }
 
     CompletableFuture<Result> send() {
-        return client.sendAsync(this.request, BodyHandler.<Void>discard(null))
-                .thenCompose(this::resultFrom);
+        PrivilegedAction<CompletableFuture<Result>> pa = () ->
+                client.sendAsync(this.request, BodyHandler.<Void>discard(null))
+                      .thenCompose(this::resultFrom);
+        return AccessController.doPrivileged(pa);
     }
 
     /*
@@ -247,7 +251,8 @@
         String expected = Base64.getEncoder().encodeToString(this.sha1.digest());
         String actual = requireSingle(headers, HEADER_ACCEPT);
         if (!actual.trim().equals(expected)) {
-            throw checkFailed("Bad " + HEADER_ACCEPT);
+            throw checkFailed("Bad " + HEADER_ACCEPT + ", expected:["
+                              + expected + "] ,got:[" + actual.trim() + "]");
         }
         String subprotocol = checkAndReturnSubprotocol(headers);
         RawChannel channel = ((RawChannel.Provider) response).rawChannel();
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/WebSocketImpl.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/WebSocketImpl.java	Sun Nov 05 21:19:55 2017 +0000
@@ -26,9 +26,17 @@
 package jdk.incubator.http.internal.websocket;
 
 import java.io.IOException;
+import java.net.InetSocketAddress;
 import java.net.ProtocolException;
+import java.net.Proxy;
+import java.net.ProxySelector;
 import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLPermission;
 import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
 import java.util.Queue;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
@@ -36,11 +44,14 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 import java.util.function.Function;
+
+import jdk.incubator.http.HttpClient;
 import jdk.incubator.http.WebSocket;
 import jdk.incubator.http.internal.common.Log;
 import jdk.incubator.http.internal.common.Pair;
 import jdk.incubator.http.internal.common.SequentialScheduler;
 import jdk.incubator.http.internal.common.SequentialScheduler.DeferredCompleter;
+import jdk.incubator.http.internal.common.Utils;
 import jdk.incubator.http.internal.websocket.OpeningHandshake.Result;
 import jdk.incubator.http.internal.websocket.OutgoingMessage.Binary;
 import jdk.incubator.http.internal.websocket.OutgoingMessage.Close;
@@ -51,8 +62,8 @@
 
 import static java.util.Objects.requireNonNull;
 import static java.util.concurrent.CompletableFuture.failedFuture;
+import static java.util.stream.Collectors.joining;
 import static jdk.incubator.http.internal.common.Pair.pair;
-import jdk.incubator.http.internal.common.Utils;
 import static jdk.incubator.http.internal.websocket.StatusCodes.CLOSED_ABNORMALLY;
 import static jdk.incubator.http.internal.websocket.StatusCodes.NO_STATUS_CODE;
 import static jdk.incubator.http.internal.websocket.StatusCodes.isLegalToSendFromClient;
@@ -105,7 +116,87 @@
     private final CompletableFuture<?> closeReceived = new CompletableFuture<>();
     private final CompletableFuture<?> closeSent = new CompletableFuture<>();
 
+    /** Returns the security permission required for the given details. */
+    static URLPermission permissionForServer(URI uri,
+                                             Collection<Pair<String, String>> headers) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(uri.getScheme()).append("://")
+          .append(uri.getAuthority())
+          .append(uri.getPath());
+        String urlstring = sb.toString();
+
+        String actionstring = headers.stream()
+                .map(p -> p.first)
+                .distinct()
+                .collect(joining(","));
+        if (actionstring != null && !actionstring.equals(""))
+            actionstring = ":" + actionstring;     // Note: no method in the action string
+
+        return new URLPermission(urlstring, actionstring);
+    }
+
+    /**
+     * Returns the security permissions required to connect to the proxy, or
+     * null if none is required or applicable.
+     */
+    static URLPermission permissionForProxy(Proxy proxy) {
+        InetSocketAddress proxyAddress = (InetSocketAddress)proxy.address();
+
+        StringBuilder sb = new StringBuilder();
+        sb.append("socket://")
+          .append(proxyAddress.getHostString()).append(":")
+          .append(proxyAddress.getPort());
+        String urlstring = sb.toString();
+        return new URLPermission(urlstring.toString(), "CONNECT");
+    }
+
+    /**
+     * Returns the proxy for the given URI when sent through the given client,
+     * or null if none is required or applicable.
+     */
+    static Proxy proxyFor(HttpClient client, URI uri) {
+        Optional<ProxySelector> optional = client.proxy();
+        if (!optional.isPresent())
+            return null;
+
+        uri = OpeningHandshake.createRequestURI(uri);  // based on the HTTP scheme
+        List<Proxy> pl = optional.get().select(uri);
+        if (pl.size() < 1)
+            return null;
+
+        Proxy proxy = pl.get(0);
+        if (!proxy.type().equals(Proxy.Type.HTTP))
+            return null;
+
+        return proxy;
+    }
+
+    /**
+     * Performs the necessary security permissions checks to connect ( possibly
+     * through a proxy ) to the builders WebSocket URI.
+     *
+     * @throws SecurityException if the security manager denies access
+     */
+    static void checkPermissions(BuilderImpl b, Proxy proxy) {
+        SecurityManager sm = System.getSecurityManager();
+        if (sm != null) {
+            sm.checkPermission(permissionForServer(b.getUri(), b.getHeaders()));
+            if (proxy != null) {
+                URLPermission perm = permissionForProxy(proxy);
+                if (perm != null)
+                    sm.checkPermission(perm);
+            }
+        }
+    }
+
     static CompletableFuture<WebSocket> newInstanceAsync(BuilderImpl b) {
+        Proxy proxy = proxyFor(b.getClient(), b.getUri());
+        try {
+            checkPermissions(b, proxy);
+        } catch (Throwable throwable) {
+            return failedFuture(throwable);
+        }
+
         Function<Result, WebSocket> newWebSocket = r -> {
             WebSocketImpl ws = new WebSocketImpl(b.getUri(),
                                                  r.subprotocol,
@@ -119,7 +210,7 @@
         };
         OpeningHandshake h;
         try {
-            h = new OpeningHandshake(b);
+            h = new OpeningHandshake(b, proxy);
         } catch (IllegalArgumentException e) {
             return failedFuture(e);
         }
--- a/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/WebSocketRequest.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/WebSocketRequest.java	Sun Nov 05 21:19:55 2017 +0000
@@ -25,6 +25,8 @@
 
 package jdk.incubator.http.internal.websocket;
 
+import java.net.Proxy;
+
 /*
  * https://tools.ietf.org/html/rfc6455#section-4.1
  */
@@ -41,4 +43,9 @@
      * WebSocket specification.
      */
     void setSystemHeader(String name, String value);
+
+    /*
+     * Sets the proxy for this request.
+     */
+    void setProxy(Proxy proxy);
 }
--- a/test/jdk/java/net/httpclient/ProxyAuthTest.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/test/jdk/java/net/httpclient/ProxyAuthTest.java	Sun Nov 05 21:19:55 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2017, 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
@@ -29,7 +29,6 @@
  * @modules java.base/sun.net.www
  *          jdk.incubator.httpclient
  * @summary Verify that Proxy-Authenticate header is correctly handled
- *
  * @run main/othervm ProxyAuthTest
  */
 
@@ -42,14 +41,17 @@
 import java.net.Authenticator;
 import java.net.InetSocketAddress;
 import java.net.PasswordAuthentication;
+import java.net.Proxy;
 import java.net.ProxySelector;
 import java.net.ServerSocket;
 import java.net.Socket;
+import java.net.SocketAddress;
 import java.net.URI;
 import jdk.incubator.http.HttpClient;
 import jdk.incubator.http.HttpRequest;
 import jdk.incubator.http.HttpResponse;
 import java.util.Base64;
+import java.util.List;
 import sun.net.www.MessageHeader;
 import static jdk.incubator.http.HttpResponse.BodyHandler.discard;
 
@@ -68,8 +70,9 @@
             InetSocketAddress paddr = new InetSocketAddress("localhost", port);
 
             URI uri = new URI("http://www.google.ie/");
+            CountingProxySelector ps = CountingProxySelector.of(paddr);
             HttpClient client = HttpClient.newBuilder()
-                                          .proxy(ProxySelector.of(paddr))
+                                          .proxy(ps)
                                           .authenticator(auth)
                                           .build();
             HttpRequest req = HttpRequest.newBuilder(uri).GET().build();
@@ -87,6 +90,9 @@
             if (!proxy.matched) {
                 throw new RuntimeException("Proxy authentication failed");
             }
+            if (ps.count() > 1) {
+                throw new RuntimeException("CountingProxySelector. Expected 1, got " + ps.count());
+            }
         }
     }
 
@@ -102,6 +108,37 @@
         }
     }
 
+    /**
+     * A Proxy Selector that wraps a ProxySelector.of(), and counts the number
+     * of times its select method has been invoked. This can be used to ensure
+     * that the Proxy Selector is invoked only once per HttpClient.sendXXX
+     * invocation.
+     */
+    static class CountingProxySelector extends ProxySelector {
+        private final ProxySelector proxySelector;
+        private volatile int count; // 0
+        private CountingProxySelector(InetSocketAddress proxyAddress) {
+            proxySelector = ProxySelector.of(proxyAddress);
+        }
+
+        public static CountingProxySelector of(InetSocketAddress proxyAddress) {
+            return new CountingProxySelector(proxyAddress);
+        }
+
+        int count() { return count; }
+
+        @Override
+        public List<Proxy> select(URI uri) {
+            count++;
+            return proxySelector.select(uri);
+        }
+
+        @Override
+        public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
+            proxySelector.connectFailed(uri, sa, ioe);
+        }
+    }
+
     static class MyProxy implements Runnable {
         final ServerSocket ss;
         private volatile boolean matched;
--- a/test/jdk/java/net/httpclient/ProxyServer.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/test/jdk/java/net/httpclient/ProxyServer.java	Sun Nov 05 21:19:55 2017 +0000
@@ -207,7 +207,7 @@
                 } else {
                     doProxy(params[1], buf, p, cmd);
                 }
-            } catch (IOException e) {
+            } catch (Throwable e) {
                 if (debug) {
                     System.out.println (e);
                 }
--- a/test/jdk/java/net/httpclient/ProxyTest.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/test/jdk/java/net/httpclient/ProxyTest.java	Sun Nov 05 21:19:55 2017 +0000
@@ -41,10 +41,12 @@
 import java.net.ProxySelector;
 import java.net.ServerSocket;
 import java.net.Socket;
+import java.net.SocketAddress;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
 import java.security.NoSuchAlgorithmException;
+import java.util.List;
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLContext;
@@ -121,6 +123,37 @@
         }
     }
 
+    /**
+     * A Proxy Selector that wraps a ProxySelector.of(), and counts the number
+     * of times its select method has been invoked. This can be used to ensure
+     * that the Proxy Selector is invoked only once per HttpClient.sendXXX
+     * invocation.
+     */
+    static class CountingProxySelector extends ProxySelector {
+        private final ProxySelector proxySelector;
+        private volatile int count; // 0
+        private CountingProxySelector(InetSocketAddress proxyAddress) {
+            proxySelector = ProxySelector.of(proxyAddress);
+        }
+
+        public static CountingProxySelector of(InetSocketAddress proxyAddress) {
+            return new CountingProxySelector(proxyAddress);
+        }
+
+        int count() { return count; }
+
+        @Override
+        public List<Proxy> select(URI uri) {
+            count++;
+            return proxySelector.select(uri);
+        }
+
+        @Override
+        public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
+            proxySelector.connectFailed(uri, sa, ioe);
+        }
+    }
+
     public static void test(HttpServer server, HttpClient.Version version)
             throws IOException,
             URISyntaxException,
@@ -158,7 +191,7 @@
             System.out.println("\nReal test begins here.");
             System.out.println("Setting up request with HttpClient for version: "
                     + version.name());
-            ProxySelector ps = ProxySelector.of(
+            CountingProxySelector ps = CountingProxySelector.of(
                     InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort()));
             HttpClient client = HttpClient.newBuilder()
                 .version(version)
@@ -178,6 +211,9 @@
             if (!RESPONSE.equals(resp)) {
                 throw new AssertionError("Unexpected response");
             }
+            if (ps.count() > 1) {
+                throw new AssertionError("CountingProxySelector. Expected 1, got " + ps.count());
+            }
         } finally {
             System.out.println("Stopping proxy");
             proxy.stop();
--- a/test/jdk/java/net/httpclient/RequestBuilderTest.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/test/jdk/java/net/httpclient/RequestBuilderTest.java	Sun Nov 05 21:19:55 2017 +0000
@@ -37,6 +37,7 @@
 import static jdk.incubator.http.HttpClient.Version.HTTP_1_1;
 import static jdk.incubator.http.HttpClient.Version.HTTP_2;
 import static jdk.incubator.http.HttpRequest.BodyPublisher.fromString;
+import static jdk.incubator.http.HttpRequest.noBody;
 import static jdk.incubator.http.HttpRequest.newBuilder;
 import static org.testng.Assert.*;
 import org.testng.annotations.Test;
@@ -166,6 +167,18 @@
         request = newBuilder(uri).GET().DELETE(fromString("")).build();
         assertEquals(request.method(), "DELETE");
         assertTrue(request.bodyPublisher().isPresent());
+
+        // CONNECT is disallowed in the implementation, since it is used for
+        // tunneling, and is handled separately for security checks.
+        assertThrows(IAE, () -> newBuilder(uri).method("CONNECT", noBody()).build());
+
+        request = newBuilder(uri).method("GET", noBody()).build();
+        assertEquals(request.method(), "GET");
+        assertTrue(request.bodyPublisher().isPresent());
+
+        request = newBuilder(uri).method("POST", fromString("")).build();
+        assertEquals(request.method(), "POST");
+        assertTrue(request.bodyPublisher().isPresent());
     }
 
     @Test
--- a/test/jdk/java/net/httpclient/SmokeTest.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/test/jdk/java/net/httpclient/SmokeTest.java	Sun Nov 05 21:19:55 2017 +0000
@@ -43,6 +43,8 @@
 import com.sun.net.httpserver.HttpsConfigurator;
 import com.sun.net.httpserver.HttpsParameters;
 import com.sun.net.httpserver.HttpsServer;
+import java.net.Proxy;
+import java.net.SocketAddress;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.net.InetSocketAddress;
 import java.net.PasswordAuthentication;
@@ -371,6 +373,37 @@
         System.out.println(" OK");
     }
 
+    /**
+     * A Proxy Selector that wraps a ProxySelector.of(), and counts the number
+     * of times its select method has been invoked. This can be used to ensure
+     * that the Proxy Selector is invoked only once per HttpClient.sendXXX
+     * invocation.
+     */
+    static class CountingProxySelector extends ProxySelector {
+        private final ProxySelector proxySelector;
+        private volatile int count; // 0
+        private CountingProxySelector(InetSocketAddress proxyAddress) {
+            proxySelector = ProxySelector.of(proxyAddress);
+        }
+
+        public static CountingProxySelector of(InetSocketAddress proxyAddress) {
+            return new CountingProxySelector(proxyAddress);
+        }
+
+        int count() { return count; }
+
+        @Override
+        public List<Proxy> select(URI uri) {
+            count++;
+            return proxySelector.select(uri);
+        }
+
+        @Override
+        public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
+            proxySelector.connectFailed(uri, sa, ioe);
+        }
+    }
+
     // Proxies
     static void test4(String s) throws Exception {
         System.out.print("test4: " + s);
@@ -380,9 +413,10 @@
 
         ExecutorService e = Executors.newCachedThreadPool();
 
+        CountingProxySelector ps = CountingProxySelector.of(proxyAddr);
         HttpClient cl = HttpClient.newBuilder()
                                   .executor(e)
-                                  .proxy(ProxySelector.of(proxyAddr))
+                                  .proxy(ps)
                                   .sslContext(ctx)
                                   .sslParameters(sslparams)
                                   .build();
@@ -400,6 +434,9 @@
             throw new RuntimeException(
                     "Body mismatch: expected [" + body + "], got [" + fc + "]");
         }
+        if (ps.count() > 1) {
+            throw new RuntimeException("CountingProxySelector. Expected 1, got " + ps.count());
+        }
         e.shutdownNow();
         System.out.println(" OK");
     }
--- a/test/jdk/java/net/httpclient/security/Driver.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/test/jdk/java/net/httpclient/security/Driver.java	Sun Nov 05 21:19:55 2017 +0000
@@ -52,8 +52,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
-
-import jdk.testlibrary.OutputAnalyzer;
 import jdk.testlibrary.Utils;
 
 /**
@@ -123,6 +121,8 @@
         while (retval == 10) {
             List<String> cmd = new ArrayList<>();
             cmd.add(javaCmd);
+            cmd.add("-ea");
+            cmd.add("-esa");
             cmd.add("-Dtest.jdk=" + testJdk);
             cmd.add("-Dtest.src=" + testSrc);
             cmd.add("-Dtest.classes=" + testClasses);
--- a/test/jdk/java/net/httpclient/security/Security.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/test/jdk/java/net/httpclient/security/Security.java	Sun Nov 05 21:19:55 2017 +0000
@@ -381,7 +381,7 @@
             r.execute();
             if (!succeeds) {
                 System.out.println("FAILED: expected security exception");
-                throw new RuntimeException("Failed");
+                throw new RuntimeException("FAILED: expected security exception\"");
             }
             System.out.println (policy + " succeeded as expected");
         } catch (BindException e) {
--- a/test/jdk/java/net/httpclient/websocket/ConnectionHandover.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/test/jdk/java/net/httpclient/websocket/ConnectionHandover.java	Sun Nov 05 21:19:55 2017 +0000
@@ -31,14 +31,10 @@
  * @test
  * @bug 8164625
  * @summary Verifies HttpClient yields the connection to the WebSocket
+ * @build DummyWebSocketServer
  * @run main/othervm -Djdk.httpclient.HttpClient.log=trace ConnectionHandover
  */
 public class ConnectionHandover {
-
-    static {
-        LoggingHelper.setupLogging();
-    }
-
     /*
      * An I/O channel associated with the connection is closed by WebSocket.abort().
      * If this connection is returned to the connection pool, then the second
--- a/test/jdk/java/net/httpclient/websocket/DummyWebSocketServer.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/test/jdk/java/net/httpclient/websocket/DummyWebSocketServer.java	Sun Nov 05 21:19:55 2017 +0000
@@ -34,6 +34,7 @@
 import java.nio.charset.CharacterCodingException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Base64;
 import java.util.HashMap;
@@ -47,9 +48,7 @@
 import java.util.stream.Collectors;
 
 import static java.lang.String.format;
-import static java.lang.System.Logger.Level.ERROR;
-import static java.lang.System.Logger.Level.INFO;
-import static java.lang.System.Logger.Level.TRACE;
+import static java.lang.System.err;
 import static java.nio.charset.StandardCharsets.ISO_8859_1;
 import static java.util.Arrays.asList;
 import static java.util.Objects.requireNonNull;
@@ -83,7 +82,6 @@
  */
 public final class DummyWebSocketServer implements Closeable {
 
-    private final static System.Logger log = System.getLogger(DummyWebSocketServer.class.getName());
     private final AtomicBoolean started = new AtomicBoolean();
     private final Thread thread;
     private volatile ServerSocketChannel ssc;
@@ -98,9 +96,9 @@
         thread = new Thread(() -> {
             try {
                 while (!Thread.currentThread().isInterrupted()) {
-                    log.log(INFO, "Accepting next connection at: " + ssc);
+                    err.println("Accepting next connection at: " + ssc);
                     SocketChannel channel = ssc.accept();
-                    log.log(INFO, "Accepted: " + channel);
+                    err.println("Accepted: " + channel);
                     try {
                         channel.configureBlocking(true);
                         StringBuilder request = new StringBuilder();
@@ -117,18 +115,18 @@
                             b.clear();
                         }
                     } catch (IOException e) {
-                        log.log(TRACE, () -> "Error in connection: " + channel, e);
+                        err.println("Error in connection: " + channel + ", " + e);
                     } finally {
-                        log.log(INFO, "Closed: " + channel);
+                        err.println("Closed: " + channel);
                         close(channel);
                     }
                 }
             } catch (ClosedByInterruptException ignored) {
             } catch (IOException e) {
-                log.log(ERROR, e);
+                err.println(e);
             } finally {
                 close(ssc);
-                log.log(INFO, "Stopped at: " + getURI());
+                err.println("Stopped at: " + getURI());
             }
         });
         thread.setName("DummyWebSocketServer");
@@ -136,7 +134,7 @@
     }
 
     public void open() throws IOException {
-        log.log(INFO, "Starting");
+        err.println("Starting");
         if (!started.compareAndSet(false, true)) {
             throw new IllegalStateException("Already started");
         }
@@ -149,12 +147,12 @@
         } catch (IOException e) {
             close(ssc);
         }
-        log.log(INFO, "Started at: " + getURI());
+        err.println("Started at: " + getURI());
     }
 
     @Override
     public void close() {
-        log.log(INFO, "Stopping: " + getURI());
+        err.println("Stopping: " + getURI());
         thread.interrupt();
         close(ssc);
     }
@@ -210,12 +208,13 @@
             if (!iterator.hasNext()) {
                 throw new IllegalStateException("The request is empty");
             }
-            if (!"GET / HTTP/1.1".equals(iterator.next())) {
+            String statusLine = iterator.next();
+            if (!(statusLine.startsWith("GET /") && statusLine.endsWith(" HTTP/1.1"))) {
                 throw new IllegalStateException
                         ("Unexpected status line: " + request.get(0));
             }
             response.add("HTTP/1.1 101 Switching Protocols");
-            Map<String, String> requestHeaders = new HashMap<>();
+            Map<String, List<String>> requestHeaders = new HashMap<>();
             while (iterator.hasNext()) {
                 String header = iterator.next();
                 String[] split = header.split(": ");
@@ -224,10 +223,8 @@
                             ("Unexpected header: " + header
                                      + ", split=" + Arrays.toString(split));
                 }
-                if (requestHeaders.put(split[0], split[1]) != null) {
-                    throw new IllegalStateException
-                            ("Duplicating headers: " + Arrays.toString(split));
-                }
+                requestHeaders.computeIfAbsent(split[0], k -> new ArrayList<>()).add(split[1]);
+
             }
             if (requestHeaders.containsKey("Sec-WebSocket-Protocol")) {
                 throw new IllegalStateException("Subprotocols are not expected");
@@ -240,17 +237,20 @@
             expectHeader(requestHeaders, "Upgrade", "websocket");
             response.add("Upgrade: websocket");
             expectHeader(requestHeaders, "Sec-WebSocket-Version", "13");
-            String key = requestHeaders.get("Sec-WebSocket-Key");
-            if (key == null) {
+            List<String> key = requestHeaders.get("Sec-WebSocket-Key");
+            if (key == null || key.isEmpty()) {
                 throw new IllegalStateException("Sec-WebSocket-Key is missing");
             }
+            if (key.size() != 1) {
+                throw new IllegalStateException("Sec-WebSocket-Key has too many values : " + key);
+            }
             MessageDigest sha1 = null;
             try {
                 sha1 = MessageDigest.getInstance("SHA-1");
             } catch (NoSuchAlgorithmException e) {
                 throw new InternalError(e);
             }
-            String x = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+            String x = key.get(0) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
             sha1.update(x.getBytes(ISO_8859_1));
             String v = Base64.getEncoder().encodeToString(sha1.digest());
             response.add("Sec-WebSocket-Accept: " + v);
@@ -258,17 +258,17 @@
         };
     }
 
-    protected static String expectHeader(Map<String, String> headers,
+    protected static String expectHeader(Map<String, List<String>> headers,
                                          String name,
                                          String value) {
-        String v = headers.get(name);
-        if (!value.equals(v)) {
+        List<String> v = headers.get(name);
+        if (!v.contains(value)) {
             throw new IllegalStateException(
                     format("Expected '%s: %s', actual: '%s: %s'",
                            name, value, name, v)
             );
         }
-        return v;
+        return value;
     }
 
     private static void close(AutoCloseable... acs) {
--- a/test/jdk/java/net/httpclient/websocket/jdk.incubator.httpclient/jdk/incubator/http/internal/websocket/BuildingWebSocketTest.java	Sun Nov 05 17:32:13 2017 +0000
+++ b/test/jdk/java/net/httpclient/websocket/jdk.incubator.httpclient/jdk/incubator/http/internal/websocket/BuildingWebSocketTest.java	Sun Nov 05 21:19:55 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2017, 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
@@ -89,9 +89,9 @@
 
     @Test(dataProvider = "badURIs")
     void illegalURI(String u) {
-        WebSocket.Builder b = HttpClient.newHttpClient()
-                .newWebSocketBuilder(URI.create(u), listener());
-        assertCompletesExceptionally(IllegalArgumentException.class, b.buildAsync());
+        assertThrows(IllegalArgumentException.class,
+                () -> HttpClient.newHttpClient()
+                                .newWebSocketBuilder(URI.create(u), listener()));
     }
 
     @Test
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/websocket/security/WSURLPermissionTest.java	Sun Nov 05 21:19:55 2017 +0000
@@ -0,0 +1,579 @@
+/*
+ * Copyright (c) 2017, 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
+ * @summary Basic security checks for WebSocket URI from the Builder
+ * @compile ../DummyWebSocketServer.java ../../ProxyServer.java
+ * @run testng/othervm/java.security.policy=httpclient.policy WSURLPermissionTest
+ */
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.net.URLPermission;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.Permission;
+import java.security.Permissions;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.security.ProtectionDomain;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.WebSocket;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import static org.testng.Assert.*;
+
+public class WSURLPermissionTest {
+
+    static AccessControlContext withPermissions(Permission... perms) {
+        Permissions p = new Permissions();
+        for (Permission perm : perms) {
+            p.add(perm);
+        }
+        ProtectionDomain pd = new ProtectionDomain(null, p);
+        return new AccessControlContext(new ProtectionDomain[]{ pd });
+    }
+
+    static AccessControlContext noPermissions() {
+        return withPermissions(/*empty*/);
+    }
+
+    URI wsURI;
+    DummyWebSocketServer webSocketServer;
+    InetSocketAddress proxyAddress;
+
+    @BeforeTest
+    public void setup() throws Exception {
+        ProxyServer proxyServer = new ProxyServer(0, true);
+        proxyAddress = new InetSocketAddress("127.0.0.1", proxyServer.getPort());
+        webSocketServer = new DummyWebSocketServer();
+        webSocketServer.open();
+        wsURI = webSocketServer.getURI();
+
+        System.out.println("Proxy Server: " + proxyAddress);
+        System.out.println("DummyWebSocketServer: " + wsURI);
+    }
+
+    @AfterTest
+    public void teardown() {
+        webSocketServer.close();
+    }
+
+    static class NoOpListener implements WebSocket.Listener {}
+    static final WebSocket.Listener noOpListener = new NoOpListener();
+
+    @DataProvider(name = "passingScenarios")
+    public Object[][] passingScenarios() {
+        HttpClient noProxyClient = HttpClient.newHttpClient();
+        return new Object[][]{
+            { (PrivilegedExceptionAction<?>)() -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .buildAsync().get().abort();
+                 return null; },                                       // no actions
+              new URLPermission[] { new URLPermission(wsURI.toString()) },
+              "0"  /* for log file identification */ },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .buildAsync().get().abort();
+                 return null; },                                       // scheme wildcard
+              new URLPermission[] { new URLPermission("ws://*") },
+              "0.1" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .buildAsync().get().abort();
+                 return null; },                                       // port wildcard
+              new URLPermission[] { new URLPermission("ws://"+wsURI.getHost()+":*") },
+              "0.2" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .buildAsync().get().abort();
+                 return null; },                                        // empty actions
+              new URLPermission[] { new URLPermission(wsURI.toString(), "") },
+              "1" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .buildAsync().get().abort();
+                 return null; },                                         // colon
+              new URLPermission[] { new URLPermission(wsURI.toString(), ":") },
+              "2" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .buildAsync().get().abort();
+                 return null; },                                        // wildcard
+              new URLPermission[] { new URLPermission(wsURI.toString(), "*:*") },
+              "3" },
+
+            // WS permission checking is agnostic of method, any/none will do
+            { (PrivilegedExceptionAction<?>)() -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .buildAsync().get().abort();
+                 return null; },                                        // specific method
+              new URLPermission[] { new URLPermission(wsURI.toString(), "GET") },
+              "3.1" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .buildAsync().get().abort();
+                 return null; },                                        // specific method
+              new URLPermission[] { new URLPermission(wsURI.toString(), "POST") },
+              "3.2" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                URI uriWithPath = wsURI.resolve("/path/x");
+                 noProxyClient.newWebSocketBuilder(uriWithPath, noOpListener)
+                              .buildAsync().get().abort();
+                 return null; },                                       // path
+              new URLPermission[] { new URLPermission(wsURI.resolve("/path/x").toString()) },
+              "4" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                URI uriWithPath = wsURI.resolve("/path/x");
+                 noProxyClient.newWebSocketBuilder(uriWithPath, noOpListener)
+                              .buildAsync().get().abort();
+                 return null; },                                       // same dir wildcard
+              new URLPermission[] { new URLPermission(wsURI.resolve("/path/*").toString()) },
+              "5" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                URI uriWithPath = wsURI.resolve("/path/x");
+                 noProxyClient.newWebSocketBuilder(uriWithPath, noOpListener)
+                              .buildAsync().get().abort();
+                 return null; },                                       // recursive
+              new URLPermission[] { new URLPermission(wsURI.resolve("/path/-").toString()) },
+              "6" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                URI uriWithPath = wsURI.resolve("/path/x");
+                 noProxyClient.newWebSocketBuilder(uriWithPath, noOpListener)
+                              .buildAsync().get().abort();
+                 return null; },                                       // recursive top
+              new URLPermission[] { new URLPermission(wsURI.resolve("/-").toString()) },
+              "7" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .header("A-Header", "A-Value")  // header
+                              .buildAsync().get().abort();
+                 return null; },
+              new URLPermission[] { new URLPermission(wsURI.toString(), ":A-Header") },
+              "8" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .header("A-Header", "A-Value")  // header
+                              .buildAsync().get().abort();
+                 return null; },                                        // wildcard
+              new URLPermission[] { new URLPermission(wsURI.toString(), ":*") },
+              "9" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .header("A-Header", "A-Value")  // headers
+                              .header("B-Header", "B-Value")  // headers
+                              .buildAsync().get().abort();
+                 return null; },
+              new URLPermission[] { new URLPermission(wsURI.toString(), ":A-Header,B-Header") },
+              "10" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .header("A-Header", "A-Value")  // headers
+                              .header("B-Header", "B-Value")  // headers
+                              .buildAsync().get().abort();
+                 return null; },                                        // wildcard
+              new URLPermission[] { new URLPermission(wsURI.toString(), ":*") },
+              "11" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .header("A-Header", "A-Value")  // headers
+                              .header("B-Header", "B-Value")  // headers
+                              .buildAsync().get().abort();
+                 return null; },                                        // wildcards
+              new URLPermission[] { new URLPermission(wsURI.toString(), "*:*") },
+              "12" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .header("A-Header", "A-Value")  // multi-value
+                              .header("A-Header", "B-Value")  // headers
+                              .buildAsync().get().abort();
+                 return null; },                                        // wildcard
+              new URLPermission[] { new URLPermission(wsURI.toString(), ":*") },
+              "13" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .header("A-Header", "A-Value")  // multi-value
+                              .header("A-Header", "B-Value")  // headers
+                              .buildAsync().get().abort();
+                 return null; },                                        // single grant
+              new URLPermission[] { new URLPermission(wsURI.toString(), ":A-Header") },
+              "14" },
+
+            // client with a DIRECT proxy
+            { (PrivilegedExceptionAction<?>)() -> {
+                 ProxySelector ps = ProxySelector.of(null);
+                 HttpClient client = HttpClient.newBuilder().proxy(ps).build();
+                 client.newWebSocketBuilder(wsURI, noOpListener)
+                       .buildAsync().get().abort();
+                 return null; },
+              new URLPermission[] { new URLPermission(wsURI.toString()) },
+              "15" },
+
+            // client with a SOCKS proxy! ( expect implementation to ignore SOCKS )
+            { (PrivilegedExceptionAction<?>)() -> {
+                 ProxySelector ps = new ProxySelector() {
+                     @Override public List<Proxy> select(URI uri) {
+                         return List.of(new Proxy(Proxy.Type.SOCKS, proxyAddress)); }
+                     @Override
+                     public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { }
+                 };
+                 HttpClient client = HttpClient.newBuilder().proxy(ps).build();
+                 client.newWebSocketBuilder(wsURI, noOpListener)
+                       .buildAsync().get().abort();
+                 return null; },
+              new URLPermission[] { new URLPermission(wsURI.toString()) },
+              "16" },
+
+            // client with a HTTP/HTTPS proxy
+            { (PrivilegedExceptionAction<?>)() -> {
+                 assert proxyAddress != null;
+                 ProxySelector ps = ProxySelector.of(proxyAddress);
+                 HttpClient client = HttpClient.newBuilder().proxy(ps).build();
+                 client.newWebSocketBuilder(wsURI, noOpListener)
+                       .buildAsync().get().abort();
+                 return null; },
+              new URLPermission[] {
+                    new URLPermission(wsURI.toString()),            // CONNECT action string
+                    new URLPermission("socket://"+proxyAddress.getHostName()
+                                      +":"+proxyAddress.getPort(), "CONNECT")},
+              "17" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 assert proxyAddress != null;
+                 ProxySelector ps = ProxySelector.of(proxyAddress);
+                 HttpClient client = HttpClient.newBuilder().proxy(ps).build();
+                 client.newWebSocketBuilder(wsURI, noOpListener)
+                       .buildAsync().get().abort();
+                 return null; },
+              new URLPermission[] {
+                    new URLPermission(wsURI.toString()),            // no action string
+                    new URLPermission("socket://"+proxyAddress.getHostName()
+                                      +":"+proxyAddress.getPort())},
+              "18" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 assert proxyAddress != null;
+                 ProxySelector ps = ProxySelector.of(proxyAddress);
+                 HttpClient client = HttpClient.newBuilder().proxy(ps).build();
+                 client.newWebSocketBuilder(wsURI, noOpListener)
+                       .buildAsync().get().abort();
+                 return null; },
+              new URLPermission[] {
+                    new URLPermission(wsURI.toString()),            // wildcard headers
+                    new URLPermission("socket://"+proxyAddress.getHostName()
+                                      +":"+proxyAddress.getPort(), "CONNECT:*")},
+              "19" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 assert proxyAddress != null;
+                 CountingProxySelector ps = CountingProxySelector.of(proxyAddress);
+                 HttpClient client = HttpClient.newBuilder().proxy(ps).build();
+                 client.newWebSocketBuilder(wsURI, noOpListener)
+                       .buildAsync().get().abort();
+                 assertEquals(ps.count(), 1);  // ps.select only invoked once
+                 return null; },
+              new URLPermission[] {
+                    new URLPermission(wsURI.toString()),            // empty headers
+                    new URLPermission("socket://"+proxyAddress.getHostName()
+                                      +":"+proxyAddress.getPort(), "CONNECT:")},
+              "20" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 assert proxyAddress != null;
+                 ProxySelector ps = ProxySelector.of(proxyAddress);
+                 HttpClient client = HttpClient.newBuilder().proxy(ps).build();
+                 client.newWebSocketBuilder(wsURI, noOpListener)
+                       .buildAsync().get().abort();
+                 return null; },
+              new URLPermission[] {
+                    new URLPermission(wsURI.toString()),
+                    new URLPermission("socket://*")},               // wildcard socket URL
+              "21" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                 assert proxyAddress != null;
+                 ProxySelector ps = ProxySelector.of(proxyAddress);
+                 HttpClient client = HttpClient.newBuilder().proxy(ps).build();
+                 client.newWebSocketBuilder(wsURI, noOpListener)
+                       .buildAsync().get().abort();
+                 return null; },
+              new URLPermission[] {
+                    new URLPermission("ws://*"),                    // wildcard ws URL
+                    new URLPermission("socket://*")},               // wildcard socket URL
+              "22" },
+
+        };
+    }
+
+    @Test(dataProvider = "passingScenarios")
+    public void testWithNoSecurityManager(PrivilegedExceptionAction<?> action,
+                                          URLPermission[] unused,
+                                          String dataProviderId)
+        throws Exception
+    {
+        // sanity ( no security manager )
+        System.setSecurityManager(null);
+        try {
+            AccessController.doPrivileged(action);
+        } finally {
+            System.setSecurityManager(new SecurityManager());
+        }
+    }
+
+    @Test(dataProvider = "passingScenarios")
+    public void testWithAllPermissions(PrivilegedExceptionAction<?> action,
+                                       URLPermission[] unused,
+                                       String dataProviderId)
+        throws Exception
+    {
+        // Run with all permissions, i.e. no further restrictions than test's AllPermission
+        assert System.getSecurityManager() != null;
+        AccessController.doPrivileged(action);
+    }
+
+    @Test(dataProvider = "passingScenarios")
+    public void testWithMinimalPermissions(PrivilegedExceptionAction<?> action,
+                                           URLPermission[] perms,
+                                           String dataProviderId)
+        throws Exception
+    {
+        // Run with minimal permissions, i.e. just what is required
+        assert System.getSecurityManager() != null;
+        AccessControlContext minimalACC = withPermissions(perms);
+        AccessController.doPrivileged(action, minimalACC);
+    }
+
+    @Test(dataProvider = "passingScenarios")
+    public void testWithNoPermissions(PrivilegedExceptionAction<?> action,
+                                      URLPermission[] unused,
+                                      String dataProviderId)
+        throws Exception
+    {
+        // Run with NO permissions, i.e. expect SecurityException
+        assert System.getSecurityManager() != null;
+        try {
+            AccessController.doPrivileged(action, noPermissions());
+            fail("EXPECTED SecurityException");
+        } catch (PrivilegedActionException expected) {
+            Throwable t = expected.getCause();
+            if (t instanceof ExecutionException)
+                t = t.getCause();
+
+            if (t instanceof SecurityException)
+                System.out.println("Caught expected SE:" + expected);
+            else
+                fail("Expected SecurityException, but got: " + t);
+        }
+    }
+
+    // --- Negative tests ---
+
+    @DataProvider(name = "failingScenarios")
+    public Object[][] failingScenarios() {
+        HttpClient noProxyClient = HttpClient.newHttpClient();
+        return new Object[][]{
+            { (PrivilegedExceptionAction<?>) () -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .buildAsync().get().abort();
+                 return null;
+              },
+              new URLPermission[]{ /* no permissions */ },
+              "50"  /* for log file identification */},
+
+            { (PrivilegedExceptionAction<?>) () -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .buildAsync().get().abort();
+                 return null;
+              },                                        // wrong scheme
+              new URLPermission[]{ new URLPermission("http://*") },
+              "51" },
+
+            { (PrivilegedExceptionAction<?>) () -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .buildAsync().get().abort();
+                 return null;
+              },                                        // wrong scheme
+              new URLPermission[]{ new URLPermission("socket://*") },
+              "52" },
+
+            { (PrivilegedExceptionAction<?>) () -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .buildAsync().get().abort();
+                 return null;
+              },                                        // wrong host
+              new URLPermission[]{ new URLPermission("ws://foo.com/") },
+              "53" },
+
+            { (PrivilegedExceptionAction<?>) () -> {
+                 noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                              .buildAsync().get().abort();
+                 return null;
+              },                                        // wrong port
+              new URLPermission[]{ new URLPermission("ws://"+ wsURI.getHost()+":5") },
+              "54" },
+
+            { (PrivilegedExceptionAction<?>) () -> {
+                  noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                               .header("A-Header", "A-Value")
+                               .buildAsync().get().abort();
+                  return null;
+              },                                                    // only perm to set B not A
+              new URLPermission[] { new URLPermission(wsURI.toString(), "*:B-Header") },
+              "55" },
+
+            { (PrivilegedExceptionAction<?>) () -> {
+                  noProxyClient.newWebSocketBuilder(wsURI, noOpListener)
+                               .header("A-Header", "A-Value")
+                               .header("B-Header", "B-Value")
+                               .buildAsync().get().abort();
+                  return null;
+              },                                                    // only perm to set B not A
+              new URLPermission[] { new URLPermission(wsURI.toString(), "*:B-Header") },
+              "56" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                URI uriWithPath = wsURI.resolve("/path/x");
+                 noProxyClient.newWebSocketBuilder(uriWithPath, noOpListener)
+                              .buildAsync().get().abort();
+                 return null; },                                    // wrong path
+              new URLPermission[] { new URLPermission(wsURI.resolve("/aDiffPath/").toString()) },
+              "57" },
+
+            { (PrivilegedExceptionAction<?>)() -> {
+                URI uriWithPath = wsURI.resolve("/path/x");
+                 noProxyClient.newWebSocketBuilder(uriWithPath, noOpListener)
+                              .buildAsync().get().abort();
+                 return null; },                                    // more specific path
+              new URLPermission[] { new URLPermission(wsURI.resolve("/path/x/y").toString()) },
+              "58" },
+
+            // client with a HTTP/HTTPS proxy
+            { (PrivilegedExceptionAction<?>)() -> {
+                 assert proxyAddress != null;
+                 ProxySelector ps = ProxySelector.of(proxyAddress);
+                 HttpClient client = HttpClient.newBuilder().proxy(ps).build();
+                 client.newWebSocketBuilder(wsURI, noOpListener)
+                       .buildAsync().get().abort();
+                 return null; },                                    // missing proxy perm
+              new URLPermission[] { new URLPermission(wsURI.toString()) },
+              "100" },
+
+            // client with a HTTP/HTTPS proxy
+            { (PrivilegedExceptionAction<?>)() -> {
+                 assert proxyAddress != null;
+                 ProxySelector ps = ProxySelector.of(proxyAddress);
+                 HttpClient client = HttpClient.newBuilder().proxy(ps).build();
+                 client.newWebSocketBuilder(wsURI, noOpListener)
+                       .buildAsync().get().abort();
+                 return null; },
+              new URLPermission[] {
+                    new URLPermission(wsURI.toString()),            // missing proxy CONNECT
+                    new URLPermission("socket://*", "GET") },
+              "101" },
+        };
+    }
+
+    @Test(dataProvider = "failingScenarios")
+    public void testWithoutEnoughPermissions(PrivilegedExceptionAction<?> action,
+                                             URLPermission[] perms,
+                                             String dataProviderId)
+        throws Exception
+    {
+        // Run without Enough permissions, i.e. expect SecurityException
+        assert System.getSecurityManager() != null;
+        AccessControlContext notEnoughPermsACC = withPermissions(perms);
+        try {
+            AccessController.doPrivileged(action, notEnoughPermsACC);
+            fail("EXPECTED SecurityException");
+        } catch (PrivilegedActionException expected) {
+            Throwable t = expected.getCause();
+            if (t instanceof ExecutionException)
+                t = t.getCause();
+
+            if (t instanceof SecurityException)
+                System.out.println("Caught expected SE:" + expected);
+            else
+                fail("Expected SecurityException, but got: " + t);
+        }
+    }
+
+    /**
+     * A Proxy Selector that wraps a ProxySelector.of(), and counts the number
+     * of times its select method has been invoked. This can be used to ensure
+     * that the Proxy Selector is invoked only once per WebSocket.Builder::buildAsync
+     * invocation.
+     */
+    static class CountingProxySelector extends ProxySelector {
+        private final ProxySelector proxySelector;
+        private volatile int count; // 0
+        private CountingProxySelector(InetSocketAddress proxyAddress) {
+            proxySelector = ProxySelector.of(proxyAddress);
+        }
+
+        public static CountingProxySelector of(InetSocketAddress proxyAddress) {
+            return new CountingProxySelector(proxyAddress);
+        }
+
+        int count() { return count; }
+
+        @Override
+        public List<Proxy> select(URI uri) {
+            System.out.println("PS: uri");
+            Throwable t = new Throwable();
+            t.printStackTrace(System.out);
+            count++;
+            return proxySelector.select(uri);
+        }
+
+        @Override
+        public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
+            proxySelector.connectFailed(uri, sa, ioe);
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/websocket/security/httpclient.policy	Sun Nov 05 21:19:55 2017 +0000
@@ -0,0 +1,46 @@
+grant codeBase "jrt:/jdk.incubator.httpclient" {
+    permission java.lang.RuntimePermission "accessClassInPackage.sun.net";
+    permission java.lang.RuntimePermission "accessClassInPackage.sun.net.util";
+    permission java.lang.RuntimePermission "accessClassInPackage.sun.net.www";
+    permission java.lang.RuntimePermission "accessClassInPackage.jdk.internal.misc";
+
+    // ## why is SP not good enough. Check API @throws signatures and impl
+    permission java.net.SocketPermission "*","connect,resolve";
+    permission java.net.URLPermission "http:*","*:*";
+    permission java.net.URLPermission "https:*","*:*";
+    permission java.net.URLPermission "ws:*","*:*";
+    permission java.net.URLPermission "wss:*","*:*";
+    permission java.net.URLPermission "socket:*","CONNECT";  // proxy
+
+    // For request/response body processors, fromFile, asFile
+    permission java.io.FilePermission "<<ALL FILES>>","read,write,delete";
+
+    // ## look at the different property names!
+    permission java.util.PropertyPermission "jdk.httpclient.HttpClient.log","read";  // name!
+    permission java.util.PropertyPermission "jdk.httpclient.auth.retrylimit","read";
+    permission java.util.PropertyPermission "jdk.httpclient.connectionWindowSize","read";
+    permission java.util.PropertyPermission "jdk.httpclient.enablepush","read";
+    permission java.util.PropertyPermission "jdk.httpclient.hpack.maxheadertablesize","read";
+    permission java.util.PropertyPermission "jdk.httpclient.keepalive.timeout","read";
+    permission java.util.PropertyPermission "jdk.httpclient.maxframesize","read";
+    permission java.util.PropertyPermission "jdk.httpclient.maxstreams","read";
+    permission java.util.PropertyPermission "jdk.httpclient.redirects.retrylimit","read";
+    permission java.util.PropertyPermission "jdk.httpclient.windowsize","read";
+    permission java.util.PropertyPermission "jdk.httpclient.bufsize","read";
+    permission java.util.PropertyPermission "jdk.httpclient.internal.selector.timeout","read";
+    permission java.util.PropertyPermission "jdk.internal.httpclient.debug","read";
+    permission java.util.PropertyPermission "jdk.internal.httpclient.hpack.debug","read";
+    permission java.util.PropertyPermission "jdk.internal.httpclient.hpack.log.level","read";
+
+    // ## these permissions do not appear in the NetPermission spec!!! JDK bug?
+    permission java.net.NetPermission "getSSLContext";
+    permission java.net.NetPermission "setSSLContext";
+
+    permission java.security.SecurityPermission "createAccessControlContext";
+};
+
+// bootstrap to get the test going, it will do its own restrictions
+grant codeBase "file:${test.classes}/*" {
+    permission java.security.AllPermission;
+};
+