# HG changeset patch # User chegar # Date 1509916795 0 # Node ID 34d7cc00f87a4b18b6c30c122fc3d55456833ae0 # Parent 634d8e14c172a1a718d728afdff9a893083103be http-client-branch: WebSocket permission checks, test updates, and more diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AuthenticationFilter.java --- 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; } diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Exchange.java --- 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> headers) { + private static URLPermission permissionForServer(URI uri, + String method, + Map> 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; + } } } } diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1Request.java --- 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(); } diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2ClientImpl.java --- 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 getConnectionFor(HttpRequestImpl req) { URI uri = req.uri(); - InetSocketAddress proxy = req.proxy(client); + InetSocketAddress proxy = req.proxy(); String key = Http2Connection.keyFor(uri, proxy); synchronized (opening) { diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2Connection.java --- 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()); diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClient.java --- 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) diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpClientImpl.java --- 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 CompletableFuture> - sendAsync(HttpRequest req, BodyHandler responseHandler) + sendAsync(HttpRequest userRequest, BodyHandler 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 mex = new MultiExchange<>(req, this, responseHandler, acc); + MultiExchange mex = new MultiExchange<>(userRequest, + requestImpl, + this, + responseHandler, + acc); CompletableFuture> 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 CompletableFuture - sendAsync(HttpRequest req, MultiSubscriber responseHandler) { + sendAsync(HttpRequest userRequest, MultiSubscriber 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 mex = new MultiExchange<>(req, this, responseHandler, acc); + MultiExchange mex = new MultiExchange<>(userRequest, + requestImpl, + this, + responseHandler, + acc); CompletableFuture 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; } } diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java --- 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. diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequest.java --- 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}. * *

Examples *

{@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
          */
diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestBuilderImpl.java
--- 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 ");
+        if (method.equals("CONNECT"))
+            throw newIAE("method CONNECT is not supported");
         return method0(method, requireNonNull(body));
     }
 
diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpRequestImpl.java
--- 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 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 pa = () -> new InetSocketAddress(host, port);
             return AccessController.doPrivileged(pa);
         } else {
diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpResponse.java
--- 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
          */
diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/MultiExchange.java
--- 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 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 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();
diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java
--- 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 mulEx = new MultiExchange<>(req, client, discard(null), null);
+                MultiExchange mulEx = new MultiExchange<>(null, req, client, discard(null), null);
                 Exchange connectExchange = new Exchange<>(req, mulEx);
 
                 return connectExchange
diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/WebSocket.java
--- 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 @@
          * 
  • {@link InterruptedException} - * if the operation was interrupted *
  • {@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. + * Security checks + * contains more information relating to the security context + * in which the the listener is invoked. *
  • {@link IllegalArgumentException} - * if any of the additional {@link #header(String, String) * headers} are illegal; diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/BuilderImpl.java --- 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"); diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/OpeningHandshake.java --- 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 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 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 send() { - return client.sendAsync(this.request, BodyHandler.discard(null)) - .thenCompose(this::resultFrom); + PrivilegedAction> pa = () -> + client.sendAsync(this.request, BodyHandler.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(); diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/WebSocketImpl.java --- 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> 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 optional = client.proxy(); + if (!optional.isPresent()) + return null; + + uri = OpeningHandshake.createRequestURI(uri); // based on the HTTP scheme + List 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 newInstanceAsync(BuilderImpl b) { + Proxy proxy = proxyFor(b.getClient(), b.getUri()); + try { + checkPermissions(b, proxy); + } catch (Throwable throwable) { + return failedFuture(throwable); + } + Function 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); } diff -r 634d8e14c172 -r 34d7cc00f87a src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/websocket/WebSocketRequest.java --- 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); } diff -r 634d8e14c172 -r 34d7cc00f87a test/jdk/java/net/httpclient/ProxyAuthTest.java --- 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 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; diff -r 634d8e14c172 -r 34d7cc00f87a test/jdk/java/net/httpclient/ProxyServer.java --- 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); } diff -r 634d8e14c172 -r 34d7cc00f87a test/jdk/java/net/httpclient/ProxyTest.java --- 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 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(); diff -r 634d8e14c172 -r 34d7cc00f87a test/jdk/java/net/httpclient/RequestBuilderTest.java --- 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 diff -r 634d8e14c172 -r 34d7cc00f87a test/jdk/java/net/httpclient/SmokeTest.java --- 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 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"); } diff -r 634d8e14c172 -r 34d7cc00f87a test/jdk/java/net/httpclient/security/Driver.java --- 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 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); diff -r 634d8e14c172 -r 34d7cc00f87a test/jdk/java/net/httpclient/security/Security.java --- 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) { diff -r 634d8e14c172 -r 34d7cc00f87a test/jdk/java/net/httpclient/websocket/ConnectionHandover.java --- 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 diff -r 634d8e14c172 -r 34d7cc00f87a test/jdk/java/net/httpclient/websocket/DummyWebSocketServer.java --- 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 requestHeaders = new HashMap<>(); + Map> 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 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 headers, + protected static String expectHeader(Map> headers, String name, String value) { - String v = headers.get(name); - if (!value.equals(v)) { + List 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) { diff -r 634d8e14c172 -r 34d7cc00f87a test/jdk/java/net/httpclient/websocket/jdk.incubator.httpclient/jdk/incubator/http/internal/websocket/BuildingWebSocketTest.java --- 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 diff -r 634d8e14c172 -r 34d7cc00f87a test/jdk/java/net/httpclient/websocket/security/WSURLPermissionTest.java --- /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 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 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); + } + } +} diff -r 634d8e14c172 -r 34d7cc00f87a test/jdk/java/net/httpclient/websocket/security/httpclient.policy --- /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 "<>","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; +}; +