You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by re...@apache.org on 2017/10/13 13:42:25 UTC

svn commit: r1812129 - in /tomcat/trunk: java/org/apache/tomcat/websocket/ test/org/apache/tomcat/websocket/ webapps/docs/

Author: remm
Date: Fri Oct 13 13:42:25 2017
New Revision: 1812129

URL: http://svn.apache.org/viewvc?rev=1812129&view=rev
Log:
57767: Add support for authentication to the websocket client. Patch submitted by J Fernandez.

Added:
    tomcat/trunk/java/org/apache/tomcat/websocket/AuthenticationException.java   (with props)
    tomcat/trunk/java/org/apache/tomcat/websocket/Authenticator.java   (with props)
    tomcat/trunk/java/org/apache/tomcat/websocket/AuthenticatorFactory.java   (with props)
    tomcat/trunk/java/org/apache/tomcat/websocket/BasicAuthenticator.java   (with props)
    tomcat/trunk/java/org/apache/tomcat/websocket/DigestAuthenticator.java   (with props)
Modified:
    tomcat/trunk/java/org/apache/tomcat/websocket/Constants.java
    tomcat/trunk/java/org/apache/tomcat/websocket/LocalStrings.properties
    tomcat/trunk/java/org/apache/tomcat/websocket/WsWebSocketContainer.java
    tomcat/trunk/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java
    tomcat/trunk/webapps/docs/changelog.xml

Added: tomcat/trunk/java/org/apache/tomcat/websocket/AuthenticationException.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/AuthenticationException.java?rev=1812129&view=auto
==============================================================================
--- tomcat/trunk/java/org/apache/tomcat/websocket/AuthenticationException.java (added)
+++ tomcat/trunk/java/org/apache/tomcat/websocket/AuthenticationException.java Fri Oct 13 13:42:25 2017
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+/**
+ * Exception thrown on authentication error connecting to a remote
+ * websocket endpoint.
+ */
+public class AuthenticationException extends Exception {
+
+    private static final long serialVersionUID = 5709887412240096441L;
+
+    /**
+     * Create authentication exception.
+     * @param message the error message
+     */
+    public AuthenticationException(String message) {
+        super(message);
+    }
+
+}

Propchange: tomcat/trunk/java/org/apache/tomcat/websocket/AuthenticationException.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: tomcat/trunk/java/org/apache/tomcat/websocket/Authenticator.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/Authenticator.java?rev=1812129&view=auto
==============================================================================
--- tomcat/trunk/java/org/apache/tomcat/websocket/Authenticator.java (added)
+++ tomcat/trunk/java/org/apache/tomcat/websocket/Authenticator.java Fri Oct 13 13:42:25 2017
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Base class for the authentication methods used by the websocket client.
+ */
+public abstract class Authenticator {
+    private static final Pattern pattern = Pattern
+            .compile("(\\w+)\\s*=\\s*(\"([^\"]+)\"|([^,=\"]+))\\s*,?");
+
+    /**
+     * Generate the authentication header that will be sent to the server.
+     * @param requestUri The request URI
+     * @param WWWAuthenticate The server auth challenge
+     * @param UserProperties The user information
+     * @return The auth header
+     * @throws AuthenticationException When an error occurs
+     */
+    public abstract String getAuthorization(String requestUri, String WWWAuthenticate,
+            Map<String, Object> UserProperties) throws AuthenticationException;
+
+    /**
+     * Get the authentication method.
+     * @return the auth scheme
+     */
+    public abstract String getSchemeName();
+
+    /**
+     * Utility method to parse the authentication header.
+     * @param WWWAuthenticate The server auth challenge
+     * @return the parsed header
+     */
+    public Map<String, String> parseWWWAuthenticateHeader(String WWWAuthenticate) {
+
+        Matcher m = pattern.matcher(WWWAuthenticate);
+        Map<String, String> challenge = new HashMap<>();
+
+        while (m.find()) {
+            String key = m.group(1);
+            String qtedValue = m.group(3);
+            String value = m.group(4);
+
+            challenge.put(key, qtedValue != null ? qtedValue : value);
+
+        }
+
+        return challenge;
+
+    }
+
+}

Propchange: tomcat/trunk/java/org/apache/tomcat/websocket/Authenticator.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: tomcat/trunk/java/org/apache/tomcat/websocket/AuthenticatorFactory.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/AuthenticatorFactory.java?rev=1812129&view=auto
==============================================================================
--- tomcat/trunk/java/org/apache/tomcat/websocket/AuthenticatorFactory.java (added)
+++ tomcat/trunk/java/org/apache/tomcat/websocket/AuthenticatorFactory.java Fri Oct 13 13:42:25 2017
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.util.Iterator;
+import java.util.ServiceLoader;
+
+/**
+ * Utility method to return the appropriate authenticator according to
+ * the scheme that the server uses.
+ */
+public class AuthenticatorFactory {
+
+    /**
+     * Return a new authenticator instance.
+     * @param authScheme The scheme used
+     * @return the authenticator
+     */
+    public static Authenticator getAuthenticator(String authScheme) {
+
+        Authenticator auth = null;
+        switch (authScheme.toLowerCase()) {
+
+        case BasicAuthenticator.schemeName:
+            auth = new BasicAuthenticator();
+            break;
+
+        case DigestAuthenticator.schemeName:
+            auth = new DigestAuthenticator();
+            break;
+
+        default:
+            auth = loadAuthenticators(authScheme);
+            break;
+        }
+
+        return auth;
+
+    }
+
+    private static Authenticator loadAuthenticators(String authScheme) {
+        ServiceLoader<Authenticator> serviceLoader = ServiceLoader.load(Authenticator.class);
+        Iterator<Authenticator> auths = serviceLoader.iterator();
+
+        while (auths.hasNext()) {
+            Authenticator auth = auths.next();
+            if (auth.getSchemeName().equalsIgnoreCase(authScheme))
+                return auth;
+        }
+
+        return null;
+    }
+
+}

Propchange: tomcat/trunk/java/org/apache/tomcat/websocket/AuthenticatorFactory.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: tomcat/trunk/java/org/apache/tomcat/websocket/BasicAuthenticator.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/BasicAuthenticator.java?rev=1812129&view=auto
==============================================================================
--- tomcat/trunk/java/org/apache/tomcat/websocket/BasicAuthenticator.java (added)
+++ tomcat/trunk/java/org/apache/tomcat/websocket/BasicAuthenticator.java Fri Oct 13 13:42:25 2017
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+
+/**
+ * Authenticator supporting the BASIC auth method.
+ */
+public class BasicAuthenticator extends Authenticator {
+
+    public static final String schemeName = "basic";
+    public static final String charsetparam = "charset";
+
+    @Override
+    public String getAuthorization(String requestUri, String WWWAuthenticate,
+            Map<String, Object> userProperties) throws AuthenticationException {
+
+        String userName = (String) userProperties.get(Constants.WS_AUTHENTICATION_USER_NAME);
+        String password = (String) userProperties.get(Constants.WS_AUTHENTICATION_PASSWORD);
+
+        if (userName == null || password == null) {
+            throw new AuthenticationException(
+                    "Failed to perform Basic authentication due to  missing user/password");
+        }
+
+        Map<String, String> wwwAuthenticate = parseWWWAuthenticateHeader(WWWAuthenticate);
+
+        String userPass = userName + ":" + password;
+        Charset charset;
+
+        if (wwwAuthenticate.get(charsetparam) != null
+                && wwwAuthenticate.get(charsetparam).equalsIgnoreCase("UTF-8")) {
+            charset = StandardCharsets.UTF_8;
+        } else {
+            charset = StandardCharsets.ISO_8859_1;
+        }
+
+        String base64 = Base64.getEncoder().encodeToString(userPass.getBytes(charset));
+
+        return " Basic " + base64;
+    }
+
+    @Override
+    public String getSchemeName() {
+        return schemeName;
+    }
+
+}

Propchange: tomcat/trunk/java/org/apache/tomcat/websocket/BasicAuthenticator.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: tomcat/trunk/java/org/apache/tomcat/websocket/Constants.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/Constants.java?rev=1812129&r1=1812128&r2=1812129&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/tomcat/websocket/Constants.java (original)
+++ tomcat/trunk/java/org/apache/tomcat/websocket/Constants.java Fri Oct 13 13:42:25 2017
@@ -88,6 +88,8 @@ public class Constants {
     public static final String CONNECTION_HEADER_NAME = "Connection";
     public static final String CONNECTION_HEADER_VALUE = "upgrade";
     public static final String LOCATION_HEADER_NAME = "Location";
+    public static final String AUTHORIZATION_HEADER_NAME = "Authorization";
+    public static final String WWW_AUTHENTICATE_HEADER_NAME = "WWW-Authenticate";
     public static final String WS_VERSION_HEADER_NAME = "Sec-WebSocket-Version";
     public static final String WS_VERSION_HEADER_VALUE = "13";
     public static final String WS_KEY_HEADER_NAME = "Sec-WebSocket-Key";
@@ -117,6 +119,9 @@ public class Constants {
             "org.apache.tomcat.websocket.DEFAULT_PROCESS_PERIOD", 10)
             .intValue();
 
+    public static final String WS_AUTHENTICATION_USER_NAME = "org.apache.tomcat.websocket.WS_AUTHENTICATION_USER_NAME";
+    public static final String WS_AUTHENTICATION_PASSWORD = "org.apache.tomcat.websocket.WS_AUTHENTICATION_PASSWORD";
+
     /* Configuration for extensions
      * Note: These options are primarily present to enable this implementation
      *       to pass compliance tests. They are expected to be removed once

Added: tomcat/trunk/java/org/apache/tomcat/websocket/DigestAuthenticator.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/DigestAuthenticator.java?rev=1812129&view=auto
==============================================================================
--- tomcat/trunk/java/org/apache/tomcat/websocket/DigestAuthenticator.java (added)
+++ tomcat/trunk/java/org/apache/tomcat/websocket/DigestAuthenticator.java Fri Oct 13 13:42:25 2017
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Map;
+
+import org.apache.tomcat.util.security.MD5Encoder;
+
+/**
+ * Authenticator supporting the DIGEST auth method.
+ */
+public class DigestAuthenticator extends Authenticator {
+
+    public static final String schemeName = "digest";
+    private SecureRandom cnonceGenerator;
+    private int nonceCount = 0;
+    private long cNonce;
+
+    @Override
+    public String getAuthorization(String requestUri, String WWWAuthenticate,
+            Map<String, Object> userProperties) throws AuthenticationException {
+
+        String userName = (String) userProperties.get(Constants.WS_AUTHENTICATION_USER_NAME);
+        String password = (String) userProperties.get(Constants.WS_AUTHENTICATION_PASSWORD);
+
+        if (userName == null || password == null) {
+            throw new AuthenticationException(
+                    "Failed to perform Digest authentication due to  missing user/password");
+        }
+
+        Map<String, String> wwwAuthenticate = parseWWWAuthenticateHeader(WWWAuthenticate);
+
+        String realm = wwwAuthenticate.get("realm");
+        String nonce = wwwAuthenticate.get("nonce");
+        String messageQop = wwwAuthenticate.get("qop");
+        String algorithm = wwwAuthenticate.get("algorithm") == null ? "MD5"
+                : wwwAuthenticate.get("algorithm");
+        String opaque = wwwAuthenticate.get("opaque");
+
+        StringBuilder challenge = new StringBuilder();
+
+        if (!messageQop.isEmpty()) {
+            if (cnonceGenerator == null) {
+                cnonceGenerator = new SecureRandom();
+            }
+
+            cNonce = cnonceGenerator.nextLong();
+            nonceCount++;
+        }
+
+        challenge.append("Digest ");
+        challenge.append("username =\"" + userName + "\",");
+        challenge.append("realm=\"" + realm + "\",");
+        challenge.append("nonce=\"" + nonce + "\",");
+        challenge.append("uri=\"" + requestUri + "\",");
+
+        try {
+            challenge.append("response=\"" + calculateRequestDigest(requestUri, userName, password,
+                    realm, nonce, messageQop, algorithm) + "\",");
+        }
+
+        catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
+            throw new AuthenticationException(
+                    "Unable to generate request digest " + e.getMessage());
+        }
+
+        challenge.append("algorithm=" + algorithm + ",");
+        challenge.append("opaque=\"" + opaque + "\",");
+
+        if (!messageQop.isEmpty()) {
+            challenge.append("qop=\"" + messageQop + "\"");
+            challenge.append(",cnonce=\"" + cNonce + "\",");
+            challenge.append("nc=" + String.format("%08X", nonceCount));
+        }
+
+        return challenge.toString();
+
+    }
+
+    private String calculateRequestDigest(String requestUri, String userName, String password,
+            String realm, String nonce, String qop, String algorithm)
+            throws UnsupportedEncodingException, NoSuchAlgorithmException {
+
+        StringBuilder preDigest = new StringBuilder();
+        String A1;
+
+        if (algorithm.equalsIgnoreCase("MD5"))
+            A1 = userName + ":" + realm + ":" + password;
+
+        else
+            A1 = encodeMD5(userName + ":" + realm + ":" + password) + ":" + nonce + ":" + cNonce;
+
+        /*
+         * If the "qop" value is "auth-int", then A2 is: A2 = Method ":"
+         * digest-uri-value ":" H(entity-body) since we do not have an entity-body, A2 =
+         * Method ":" digest-uri-value for auth and auth_int
+         */
+        String A2 = "GET:" + requestUri;
+
+        preDigest.append(encodeMD5(A1));
+        preDigest.append(":");
+        preDigest.append(nonce);
+
+        if (qop.toLowerCase().contains("auth")) {
+            preDigest.append(":");
+            preDigest.append(String.format("%08X", nonceCount));
+            preDigest.append(":");
+            preDigest.append(String.valueOf(cNonce));
+            preDigest.append(":");
+            preDigest.append(qop);
+        }
+
+        preDigest.append(":");
+        preDigest.append(encodeMD5(A2));
+
+        return encodeMD5(preDigest.toString());
+
+    }
+
+    private String encodeMD5(String value)
+            throws UnsupportedEncodingException, NoSuchAlgorithmException {
+        byte[] bytesOfMessage = value.getBytes(StandardCharsets.ISO_8859_1);
+        MessageDigest md = MessageDigest.getInstance("MD5");
+        byte[] thedigest = md.digest(bytesOfMessage);
+
+        return MD5Encoder.encode(thedigest);
+    }
+
+    @Override
+    public String getSchemeName() {
+        return schemeName;
+    }
+}

Propchange: tomcat/trunk/java/org/apache/tomcat/websocket/DigestAuthenticator.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: tomcat/trunk/java/org/apache/tomcat/websocket/LocalStrings.properties
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/LocalStrings.properties?rev=1812129&r1=1812128&r2=1812129&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/tomcat/websocket/LocalStrings.properties (original)
+++ tomcat/trunk/java/org/apache/tomcat/websocket/LocalStrings.properties Fri Oct 13 13:42:25 2017
@@ -121,20 +121,24 @@ wsSession.instanceDestroy=Endpoint insta
 # as many as 4 bytes.
 wsWebSocketContainer.shutdown=The web application is stopping
 
-wsWebSocketContainer.asynchronousSocketChannelFail=Unable to open a connection to the server
 wsWebSocketContainer.defaultConfiguratorFail=Failed to create the default configurator
 wsWebSocketContainer.endpointCreateFail=Failed to create a local endpoint of type [{0}]
-wsWebSocketContainer.httpRequestFailed=The HTTP request to initiate the WebSocket connection failed
-wsWebSocketContainer.invalidExtensionParameters=The server responded with extension parameters the client is unable to support
-wsWebSocketContainer.invalidHeader=Unable to parse HTTP header as no colon is present to delimit header name and header value in [{0}]. The header has been skipped.
-wsWebSocketContainer.invalidStatus=The HTTP response from the server [{0}] did not permit the HTTP upgrade to WebSocket
-wsWebSocketContainer.invalidSubProtocol=The WebSocket server returned multiple values for the Sec-WebSocket-Protocol header
 wsWebSocketContainer.maxBuffer=This implementation limits the maximum size of a buffer to Integer.MAX_VALUE
 wsWebSocketContainer.missingAnnotation=Cannot use POJO class [{0}] as it is not annotated with @ClientEndpoint
-wsWebSocketContainer.pathNoHost=No host was specified in URI
-wsWebSocketContainer.pathWrongScheme=The scheme [{0}] is not supported. The supported schemes are ws and wss
-wsWebSocketContainer.proxyConnectFail=Failed to connect to the configured Proxy [{0}]. The HTTP response code was [{1}]
 wsWebSocketContainer.sessionCloseFail=Session with ID [{0}] did not close cleanly
-wsWebSocketContainer.sslEngineFail=Unable to create SSLEngine to support SSL/TLS connections
-wsWebSocketContainer.missingLocationHeader=Failed to handle HTTP response code [{0}]. Missing Location header in response
-wsWebSocketContainer.redirectThreshold=Cyclic Location header [{0}] detected / reached max number of redirects [{1}] of max [{2}]
\ No newline at end of file
+
+wsWebSocketClient.asynchronousSocketChannelFail=Unable to open a connection to the server
+wsWebSocketClient.httpRequestFailed=The HTTP request to initiate the WebSocket connection failed
+wsWebSocketClient.invalidExtensionParameters=The server responded with extension parameters the client is unable to support
+wsWebSocketClient.invalidHeader=Unable to parse HTTP header as no colon is present to delimit header name and header value in [{0}]. The header has been skipped.
+wsWebSocketClient.invalidStatus=The HTTP response from the server [{0}] did not permit the HTTP upgrade to WebSocket
+wsWebSocketClient.invalidSubProtocol=The WebSocket server returned multiple values for the Sec-WebSocket-Protocol header
+wsWebSocketClient.pathNoHost=No host was specified in URI
+wsWebSocketClient.pathWrongScheme=The scheme [{0}] is not supported. The supported schemes are ws and wss
+wsWebSocketClient.proxyConnectFail=Failed to connect to the configured Proxy [{0}]. The HTTP response code was [{1}]
+wsWebSocketClient.sslEngineFail=Unable to create SSLEngine to support SSL/TLS connections
+wsWebSocketClient.missingLocationHeader=Failed to handle HTTP response code [{0}]. Missing Location header in response
+wsWebSocketClient.redirectThreshold=Cyclic Location header [{0}] detected / reached max number of redirects [{1}] of max [{2}]
+wsWebSocketClient.unsupportedAuthScheme=Failed to handle HTTP response code [{0}]. Unsupported Authentication scheme [{1}] returned in response
+wsWebSocketClient.failedAuthentication=Failed to handle HTTP response code [{0}]. Authentication header was not accepted by server.
+wsWebSocketClient.missingWWWAuthenticateHeader=Failed to handle HTTP response code [{0}]. Missing WWW-Authenticate header in response
\ No newline at end of file

Modified: tomcat/trunk/java/org/apache/tomcat/websocket/WsWebSocketContainer.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/WsWebSocketContainer.java?rev=1812129&r1=1812128&r2=1812129&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/tomcat/websocket/WsWebSocketContainer.java (original)
+++ tomcat/trunk/java/org/apache/tomcat/websocket/WsWebSocketContainer.java Fri Oct 13 13:42:25 2017
@@ -76,14 +76,17 @@ import org.apache.tomcat.websocket.pojo.
 public class WsWebSocketContainer implements WebSocketContainer, BackgroundProcess {
 
     private static final StringManager sm = StringManager.getManager(WsWebSocketContainer.class);
-    private static final Random random = new Random();
-    private static final byte[] crlf = new byte[] {13, 10};
+
+    private static final Random RANDOM = new Random();
+    private static final byte[] CRLF = new byte[] { 13, 10 };
 
     private static final byte[] GET_BYTES = "GET ".getBytes(StandardCharsets.ISO_8859_1);
     private static final byte[] ROOT_URI_BYTES = "/".getBytes(StandardCharsets.ISO_8859_1);
     private static final byte[] HTTP_VERSION_BYTES =
             " HTTP/1.1\r\n".getBytes(StandardCharsets.ISO_8859_1);
 
+    private Set<URI> redirectSet = null;
+
     private volatile AsynchronousChannelGroup asynchronousChannelGroup = null;
     private final Object asynchronousChannelGroupLock = new Object();
 
@@ -99,7 +102,6 @@ public class WsWebSocketContainer implem
     private volatile long defaultMaxSessionIdleTimeout = 0;
     private int backgroundProcessCount = 0;
     private int processPeriod = Constants.DEFAULT_PROCESS_PERIOD;
-    private Set<URI> redirectSet = null;
 
     private InstanceManager instanceManager;
 
@@ -192,6 +194,178 @@ public class WsWebSocketContainer implem
     public Session connectToServer(Endpoint endpoint,
             ClientEndpointConfig clientEndpointConfiguration, URI path)
             throws DeploymentException {
+        return connectToServerRecursive(endpoint, clientEndpointConfiguration, path);
+    }
+
+    protected void registerSession(Endpoint endpoint, WsSession wsSession) {
+
+        if (!wsSession.isOpen()) {
+            // The session was closed during onOpen. No need to register it.
+            return;
+        }
+        synchronized (endPointSessionMapLock) {
+            if (endpointSessionMap.size() == 0) {
+                BackgroundProcessManager.getInstance().register(this);
+            }
+            Set<WsSession> wsSessions = endpointSessionMap.get(endpoint);
+            if (wsSessions == null) {
+                wsSessions = new HashSet<>();
+                endpointSessionMap.put(endpoint, wsSessions);
+            }
+            wsSessions.add(wsSession);
+        }
+        sessions.put(wsSession, wsSession);
+    }
+
+
+    protected void unregisterSession(Endpoint endpoint, WsSession wsSession) {
+
+        synchronized (endPointSessionMapLock) {
+            Set<WsSession> wsSessions = endpointSessionMap.get(endpoint);
+            if (wsSessions != null) {
+                wsSessions.remove(wsSession);
+                if (wsSessions.size() == 0) {
+                    endpointSessionMap.remove(endpoint);
+                }
+            }
+            if (endpointSessionMap.size() == 0) {
+                BackgroundProcessManager.getInstance().unregister(this);
+            }
+        }
+        sessions.remove(wsSession);
+    }
+
+
+    Set<Session> getOpenSessions(Endpoint endpoint) {
+        Set<Session> result = new HashSet<>();
+        synchronized (endPointSessionMapLock) {
+            Set<WsSession> sessions = endpointSessionMap.get(endpoint);
+            if (sessions != null) {
+                result.addAll(sessions);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public long getDefaultMaxSessionIdleTimeout() {
+        return defaultMaxSessionIdleTimeout;
+    }
+
+
+    @Override
+    public void setDefaultMaxSessionIdleTimeout(long timeout) {
+        this.defaultMaxSessionIdleTimeout = timeout;
+    }
+
+
+    @Override
+    public int getDefaultMaxBinaryMessageBufferSize() {
+        return maxBinaryMessageBufferSize;
+    }
+
+
+    @Override
+    public void setDefaultMaxBinaryMessageBufferSize(int max) {
+        maxBinaryMessageBufferSize = max;
+    }
+
+
+    @Override
+    public int getDefaultMaxTextMessageBufferSize() {
+        return maxTextMessageBufferSize;
+    }
+
+
+    @Override
+    public void setDefaultMaxTextMessageBufferSize(int max) {
+        maxTextMessageBufferSize = max;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     *
+     * Currently, this implementation does not support any extensions.
+     */
+    @Override
+    public Set<Extension> getInstalledExtensions() {
+        return Collections.emptySet();
+    }
+
+
+    /**
+     * {@inheritDoc}
+     *
+     * The default value for this implementation is -1.
+     */
+    @Override
+    public long getDefaultAsyncSendTimeout() {
+        return defaultAsyncTimeout;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     *
+     * The default value for this implementation is -1.
+     */
+    @Override
+    public void setAsyncSendTimeout(long timeout) {
+        this.defaultAsyncTimeout = timeout;
+    }
+
+
+    /**
+     * Cleans up the resources still in use by WebSocket sessions created from
+     * this container. This includes closing sessions and cancelling
+     * {@link Future}s associated with blocking read/writes.
+     */
+    public void destroy() {
+        CloseReason cr = new CloseReason(
+                CloseCodes.GOING_AWAY, sm.getString("wsWebSocketContainer.shutdown"));
+
+        for (WsSession session : sessions.keySet()) {
+            try {
+                session.close(cr);
+            } catch (IOException ioe) {
+                log.debug(sm.getString(
+                        "wsWebSocketContainer.sessionCloseFail", session.getId()), ioe);
+            }
+        }
+
+        // Only unregister with AsyncChannelGroupUtil if this instance
+        // registered with it
+        if (asynchronousChannelGroup != null) {
+            synchronized (asynchronousChannelGroupLock) {
+                if (asynchronousChannelGroup != null) {
+                    AsyncChannelGroupUtil.unregister();
+                    asynchronousChannelGroup = null;
+                }
+            }
+        }
+    }
+
+
+    protected AsynchronousChannelGroup getAsynchronousChannelGroup() {
+        // Use AsyncChannelGroupUtil to share a common group amongst all
+        // WebSocket clients
+        AsynchronousChannelGroup result = asynchronousChannelGroup;
+        if (result == null) {
+            synchronized (asynchronousChannelGroupLock) {
+                if (asynchronousChannelGroup == null) {
+                    asynchronousChannelGroup = AsyncChannelGroupUtil.register();
+                }
+                result = asynchronousChannelGroup;
+            }
+        }
+        return result;
+    }
+
+
+    private Session connectToServerRecursive(Endpoint endpoint,
+            ClientEndpointConfig clientEndpointConfiguration, URI path)
+            throws DeploymentException {
 
         boolean secure = false;
         ByteBuffer proxyConnect = null;
@@ -206,14 +380,14 @@ public class WsWebSocketContainer implem
             secure = true;
         } else {
             throw new DeploymentException(sm.getString(
-                    "wsWebSocketContainer.pathWrongScheme", scheme));
+                    "wsWebSocketClient.pathWrongScheme", scheme));
         }
 
         // Validate host
         String host = path.getHost();
         if (host == null) {
             throw new DeploymentException(
-                    sm.getString("wsWebSocketContainer.pathNoHost"));
+                    sm.getString("wsWebSocketClient.pathNoHost"));
         }
         int port = path.getPort();
 
@@ -256,13 +430,11 @@ public class WsWebSocketContainer implem
         }
 
         // Create the initial HTTP request to open the WebSocket connection
-        Map<String,List<String>> reqHeaders = createRequestHeaders(host, port,
-                clientEndpointConfiguration.getPreferredSubprotocols(),
-                clientEndpointConfiguration.getExtensions());
-        clientEndpointConfiguration.getConfigurator().
-                beforeRequest(reqHeaders);
-        if (Constants.DEFAULT_ORIGIN_HEADER_VALUE != null &&
-                !reqHeaders.containsKey(Constants.ORIGIN_HEADER_NAME)) {
+        Map<String, List<String>> reqHeaders = createRequestHeaders(host, port,
+                clientEndpointConfiguration);
+        clientEndpointConfiguration.getConfigurator().beforeRequest(reqHeaders);
+        if (Constants.DEFAULT_ORIGIN_HEADER_VALUE != null
+                && !reqHeaders.containsKey(Constants.ORIGIN_HEADER_NAME)) {
             List<String> originValues = new ArrayList<>(1);
             originValues.add(Constants.DEFAULT_ORIGIN_HEADER_VALUE);
             reqHeaders.put(Constants.ORIGIN_HEADER_NAME, originValues);
@@ -274,7 +446,7 @@ public class WsWebSocketContainer implem
             socketChannel = AsynchronousSocketChannel.open(getAsynchronousChannelGroup());
         } catch (IOException ioe) {
             throw new DeploymentException(sm.getString(
-                    "wsWebSocketContainer.asynchronousSocketChannelFail"), ioe);
+                    "wsWebSocketClient.asynchronousSocketChannelFail"), ioe);
         }
 
         Map<String,Object> userProperties = clientEndpointConfiguration.getUserProperties();
@@ -288,7 +460,7 @@ public class WsWebSocketContainer implem
 
         // Set-up
         // Same size as the WsFrame input buffer
-        ByteBuffer response = ByteBuffer.allocate(maxBinaryMessageBufferSize);
+        ByteBuffer response = ByteBuffer.allocate(getDefaultMaxBinaryMessageBufferSize());
         String subProtocol;
         boolean success = false;
         List<Extension> extensionsAgreed = new ArrayList<>();
@@ -307,7 +479,7 @@ public class WsWebSocketContainer implem
                 HttpResponse httpResponse = processResponse(response, channel, timeout);
                 if (httpResponse.getStatus() != 200) {
                     throw new DeploymentException(sm.getString(
-                            "wsWebSocketContainer.proxyConnectFail", selectedProxy,
+                            "wsWebSocketClient.proxyConnectFail", selectedProxy,
                             Integer.toString(httpResponse.getStatus())));
                 }
             } catch (TimeoutException | InterruptedException | ExecutionException |
@@ -316,7 +488,7 @@ public class WsWebSocketContainer implem
                     channel.close();
                 }
                 throw new DeploymentException(
-                        sm.getString("wsWebSocketContainer.httpRequestFailed"), e);
+                        sm.getString("wsWebSocketClient.httpRequestFailed"), e);
             }
         }
 
@@ -359,7 +531,7 @@ public class WsWebSocketContainer implem
                     if (locationHeader == null || locationHeader.isEmpty() ||
                             locationHeader.get(0) == null || locationHeader.get(0).isEmpty()) {
                         throw new DeploymentException(sm.getString(
-                                "wsWebSocketContainer.missingLocationHeader",
+                                "wsWebSocketClient.missingLocationHeader",
                                 Integer.toString(httpResponse.status)));
                     }
 
@@ -384,16 +556,55 @@ public class WsWebSocketContainer implem
 
                     if (!redirectSet.add(redirectLocation) || redirectSet.size() > maxRedirects) {
                         throw new DeploymentException(sm.getString(
-                                "wsWebSocketContainer.redirectThreshold", redirectLocation,
+                                "wsWebSocketClient.redirectThreshold", redirectLocation,
                                 Integer.toString(redirectSet.size()),
                                 Integer.toString(maxRedirects)));
                     }
 
-                    return connectToServer(endpoint, clientEndpointConfiguration, redirectLocation);
+                    return connectToServerRecursive(endpoint, clientEndpointConfiguration, redirectLocation);
+
+                }
+
+                else if (httpResponse.status == 401) {
+
+                    if (userProperties.get(Constants.AUTHORIZATION_HEADER_NAME) != null) {
+                        throw new DeploymentException(sm.getString(
+                                "wsWebSocketClient.failedAuthentication", httpResponse.status));
+                    }
+
+                    List<String> wwwAuthenticateHeaders = httpResponse.getHandshakeResponse()
+                            .getHeaders().get(Constants.WWW_AUTHENTICATE_HEADER_NAME);
+
+                    if (wwwAuthenticateHeaders == null || wwwAuthenticateHeaders.isEmpty() ||
+                            wwwAuthenticateHeaders.get(0) == null || wwwAuthenticateHeaders.get(0).isEmpty()) {
+                        throw new DeploymentException(sm.getString(
+                                "wsWebSocketClient.missingWWWAuthenticateHeader",
+                                Integer.toString(httpResponse.status)));
+                    }
+
+                    String authScheme = wwwAuthenticateHeaders.get(0).split("\\s+", 2)[0];
+                    String requestUri = new String(request.array(), StandardCharsets.ISO_8859_1)
+                            .split("\\s", 3)[1];
+
+                    Authenticator auth = AuthenticatorFactory.getAuthenticator(authScheme);
+
+                    if (auth == null) {
+                        throw new DeploymentException(
+                                sm.getString("wsWebSocketClient.unsupportedAuthScheme",
+                                        httpResponse.status, authScheme));
+                    }
+
+                    userProperties.put(Constants.AUTHORIZATION_HEADER_NAME, auth.getAuthorization(
+                            requestUri, wwwAuthenticateHeaders.get(0), userProperties));
+
+                    return connectToServerRecursive(endpoint, clientEndpointConfiguration, path);
+
+                }
 
+                else {
+                    throw new DeploymentException(sm.getString("wsWebSocketClient.invalidStatus",
+                            Integer.toString(httpResponse.status)));
                 }
-                throw new DeploymentException(sm.getString("wsWebSocketContainer.invalidStatus",
-                        Integer.toString(httpResponse.status)));
             }
             HandshakeResponse handshakeResponse = httpResponse.getHandshakeResponse();
             clientEndpointConfiguration.getConfigurator().afterResponse(handshakeResponse);
@@ -407,7 +618,7 @@ public class WsWebSocketContainer implem
                 subProtocol = protocolHeaders.get(0);
             } else {
                 throw new DeploymentException(
-                        sm.getString("wsWebSocketContainer.invalidSubProtocol"));
+                        sm.getString("wsWebSocketClient.invalidSubProtocol"));
             }
 
             // Extensions
@@ -429,7 +640,7 @@ public class WsWebSocketContainer implem
                 Transformation t = factory.create(extension.getName(), wrapper, false);
                 if (t == null) {
                     throw new DeploymentException(sm.getString(
-                            "wsWebSocketContainer.invalidExtensionParameters"));
+                            "wsWebSocketClient.invalidExtensionParameters"));
                 }
                 if (transformation == null) {
                     transformation = t;
@@ -440,13 +651,17 @@ public class WsWebSocketContainer implem
 
             success = true;
         } catch (ExecutionException | InterruptedException | SSLException |
-                EOFException | TimeoutException | URISyntaxException e) {
+                EOFException | TimeoutException | URISyntaxException | AuthenticationException e) {
             throw new DeploymentException(
-                    sm.getString("wsWebSocketContainer.httpRequestFailed"), e);
+                    sm.getString("wsWebSocketClient.httpRequestFailed"), e);
         } finally {
             if (!success) {
                 channel.close();
             }
+
+            if (redirectSet != null && !redirectSet.isEmpty()) {
+                redirectSet.clear();
+            }
         }
 
         // Switch to WebSocket
@@ -537,61 +752,19 @@ public class WsWebSocketContainer implem
         return ByteBuffer.wrap(bytes);
     }
 
+    private static Map<String, List<String>> createRequestHeaders(String host, int port,
+            ClientEndpointConfig clientEndpointConfiguration) {
 
-    protected void registerSession(Endpoint endpoint, WsSession wsSession) {
-
-        if (!wsSession.isOpen()) {
-            // The session was closed during onOpen. No need to register it.
-            return;
-        }
-        synchronized (endPointSessionMapLock) {
-            if (endpointSessionMap.size() == 0) {
-                BackgroundProcessManager.getInstance().register(this);
-            }
-            Set<WsSession> wsSessions = endpointSessionMap.get(endpoint);
-            if (wsSessions == null) {
-                wsSessions = new HashSet<>();
-                endpointSessionMap.put(endpoint, wsSessions);
-            }
-            wsSessions.add(wsSession);
-        }
-        sessions.put(wsSession, wsSession);
-    }
-
-
-    protected void unregisterSession(Endpoint endpoint, WsSession wsSession) {
-
-        synchronized (endPointSessionMapLock) {
-            Set<WsSession> wsSessions = endpointSessionMap.get(endpoint);
-            if (wsSessions != null) {
-                wsSessions.remove(wsSession);
-                if (wsSessions.size() == 0) {
-                    endpointSessionMap.remove(endpoint);
-                }
-            }
-            if (endpointSessionMap.size() == 0) {
-                BackgroundProcessManager.getInstance().unregister(this);
-            }
+        Map<String, List<String>> headers = new HashMap<>();
+        List<Extension> extensions = clientEndpointConfiguration.getExtensions();
+        List<String> subProtocols = clientEndpointConfiguration.getPreferredSubprotocols();
+        Map<String, Object> userProperties = clientEndpointConfiguration.getUserProperties();
+
+        if (userProperties.get(Constants.AUTHORIZATION_HEADER_NAME) != null) {
+            List<String> authValues = new ArrayList<>(1);
+            authValues.add((String) userProperties.get(Constants.AUTHORIZATION_HEADER_NAME));
+            headers.put(Constants.AUTHORIZATION_HEADER_NAME, authValues);
         }
-        sessions.remove(wsSession);
-    }
-
-
-    Set<Session> getOpenSessions(Endpoint endpoint) {
-        Set<Session> result = new HashSet<>();
-        synchronized (endPointSessionMapLock) {
-            Set<WsSession> sessions = endpointSessionMap.get(endpoint);
-            if (sessions != null) {
-                result.addAll(sessions);
-            }
-        }
-        return result;
-    }
-
-    private static Map<String,List<String>> createRequestHeaders(String host,
-            int port, List<String> subProtocols, List<Extension> extensions) {
-
-        Map<String,List<String>> headers = new HashMap<>();
 
         // Host header
         List<String> hostValues = new ArrayList<>(1);
@@ -660,7 +833,7 @@ public class WsWebSocketContainer implem
 
     private static String generateWsKeyValue() {
         byte[] keyBytes = new byte[16];
-        random.nextBytes(keyBytes);
+        RANDOM.nextBytes(keyBytes);
         return Base64.encodeBase64String(keyBytes);
     }
 
@@ -688,7 +861,7 @@ public class WsWebSocketContainer implem
         }
 
         // Terminating CRLF
-        result.put(crlf);
+        result.put(CRLF);
 
         result.flip();
 
@@ -704,7 +877,7 @@ public class WsWebSocketContainer implem
         result.put(key.getBytes(StandardCharsets.ISO_8859_1));
         result.put(": ".getBytes(StandardCharsets.ISO_8859_1));
         result.put(StringUtils.join(values).getBytes(StandardCharsets.ISO_8859_1));
-        result.put(crlf);
+        result.put(CRLF);
     }
 
 
@@ -768,13 +941,13 @@ public class WsWebSocketContainer implem
         // CONNECT for proxy may return a 1.0 response
         if (parts.length < 2 || !("HTTP/1.0".equals(parts[0]) || "HTTP/1.1".equals(parts[0]))) {
             throw new DeploymentException(sm.getString(
-                    "wsWebSocketContainer.invalidStatus", line));
+                    "wsWebSocketClient.invalidStatus", line));
         }
         try {
             return Integer.parseInt(parts[1]);
         } catch (NumberFormatException nfe) {
             throw new DeploymentException(sm.getString(
-                    "wsWebSocketContainer.invalidStatus", line));
+                    "wsWebSocketClient.invalidStatus", line));
         }
     }
 
@@ -784,7 +957,7 @@ public class WsWebSocketContainer implem
 
         int index = line.indexOf(':');
         if (index == -1) {
-            log.warn(sm.getString("wsWebSocketContainer.invalidHeader", line));
+            log.warn(sm.getString("wsWebSocketClient.invalidHeader", line));
             return;
         }
         // Header names are case insensitive so always use lower case
@@ -869,127 +1042,28 @@ public class WsWebSocketContainer implem
             return engine;
         } catch (Exception e) {
             throw new DeploymentException(sm.getString(
-                    "wsWebSocketContainer.sslEngineFail"), e);
+                    "wsWebSocketClient.sslEngineFail"), e);
         }
     }
 
+    private static class HttpResponse {
+        private final int status;
+        private final HandshakeResponse handshakeResponse;
 
-    @Override
-    public long getDefaultMaxSessionIdleTimeout() {
-        return defaultMaxSessionIdleTimeout;
-    }
-
-
-    @Override
-    public void setDefaultMaxSessionIdleTimeout(long timeout) {
-        this.defaultMaxSessionIdleTimeout = timeout;
-    }
-
-
-    @Override
-    public int getDefaultMaxBinaryMessageBufferSize() {
-        return maxBinaryMessageBufferSize;
-    }
-
-
-    @Override
-    public void setDefaultMaxBinaryMessageBufferSize(int max) {
-        maxBinaryMessageBufferSize = max;
-    }
-
-
-    @Override
-    public int getDefaultMaxTextMessageBufferSize() {
-        return maxTextMessageBufferSize;
-    }
-
-
-    @Override
-    public void setDefaultMaxTextMessageBufferSize(int max) {
-        maxTextMessageBufferSize = max;
-    }
-
-
-    /**
-     * {@inheritDoc}
-     *
-     * Currently, this implementation does not support any extensions.
-     */
-    @Override
-    public Set<Extension> getInstalledExtensions() {
-        return Collections.emptySet();
-    }
-
-
-    /**
-     * {@inheritDoc}
-     *
-     * The default value for this implementation is -1.
-     */
-    @Override
-    public long getDefaultAsyncSendTimeout() {
-        return defaultAsyncTimeout;
-    }
-
-
-    /**
-     * {@inheritDoc}
-     *
-     * The default value for this implementation is -1.
-     */
-    @Override
-    public void setAsyncSendTimeout(long timeout) {
-        this.defaultAsyncTimeout = timeout;
-    }
-
-
-    /**
-     * Cleans up the resources still in use by WebSocket sessions created from
-     * this container. This includes closing sessions and cancelling
-     * {@link Future}s associated with blocking read/writes.
-     */
-    public void destroy() {
-        CloseReason cr = new CloseReason(
-                CloseCodes.GOING_AWAY, sm.getString("wsWebSocketContainer.shutdown"));
-
-        for (WsSession session : sessions.keySet()) {
-            try {
-                session.close(cr);
-            } catch (IOException ioe) {
-                log.debug(sm.getString(
-                        "wsWebSocketContainer.sessionCloseFail", session.getId()), ioe);
-            }
+        public HttpResponse(int status, HandshakeResponse handshakeResponse) {
+            this.status = status;
+            this.handshakeResponse = handshakeResponse;
         }
 
-        // Only unregister with AsyncChannelGroupUtil if this instance
-        // registered with it
-        if (asynchronousChannelGroup != null) {
-            synchronized (asynchronousChannelGroupLock) {
-                if (asynchronousChannelGroup != null) {
-                    AsyncChannelGroupUtil.unregister();
-                    asynchronousChannelGroup = null;
-                }
-            }
+        public int getStatus() {
+            return status;
         }
-    }
 
-
-    private AsynchronousChannelGroup getAsynchronousChannelGroup() {
-        // Use AsyncChannelGroupUtil to share a common group amongst all
-        // WebSocket clients
-        AsynchronousChannelGroup result = asynchronousChannelGroup;
-        if (result == null) {
-            synchronized (asynchronousChannelGroupLock) {
-                if (asynchronousChannelGroup == null) {
-                    asynchronousChannelGroup = AsyncChannelGroupUtil.register();
-                }
-                result = asynchronousChannelGroup;
-            }
+        public HandshakeResponse getHandshakeResponse() {
+            return handshakeResponse;
         }
-        return result;
     }
 
-
     // ----------------------------------------------- BackgroundProcess methods
 
     @Override
@@ -1024,24 +1098,4 @@ public class WsWebSocketContainer implem
         return processPeriod;
     }
 
-
-    private static class HttpResponse {
-        private final int status;
-        private final HandshakeResponse handshakeResponse;
-
-        public HttpResponse(int status, HandshakeResponse handshakeResponse) {
-            this.status = status;
-            this.handshakeResponse = handshakeResponse;
-        }
-
-
-        public int getStatus() {
-            return status;
-        }
-
-
-        public HandshakeResponse getHandshakeResponse() {
-            return handshakeResponse;
-        }
-    }
 }

Modified: tomcat/trunk/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java?rev=1812129&r1=1812128&r2=1812129&view=diff
==============================================================================
--- tomcat/trunk/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java (original)
+++ tomcat/trunk/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java Fri Oct 13 13:42:25 2017
@@ -30,13 +30,22 @@ import org.junit.Assert;
 import org.junit.Test;
 
 import org.apache.catalina.Context;
+import org.apache.catalina.authenticator.AuthenticatorBase;
 import org.apache.catalina.servlets.DefaultServlet;
 import org.apache.catalina.startup.Tomcat;
+import org.apache.tomcat.util.descriptor.web.LoginConfig;
+import org.apache.tomcat.util.descriptor.web.SecurityCollection;
+import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
 import org.apache.tomcat.websocket.TesterMessageCountClient.BasicText;
 import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint;
 
 public class TestWebSocketFrameClient extends WebSocketBaseTest {
 
+    private static final String USER = "Aladdin";
+    private static final String PWD = "open sesame";
+    private static final String ROLE = "role";
+    private static final String URI_PROTECTED = "/foo";
+
     @Test
     public void testConnectToServerEndpoint() throws Exception {
         Tomcat tomcat = getTomcatInstance();
@@ -93,15 +102,19 @@ public class TestWebSocketFrameClient ex
 
         tomcat.start();
 
-        echoTester("");
-        echoTester("/");
-        echoTester("/foo");
-        echoTester("/foo/");
+        echoTester("",null);
+        echoTester("/",null);
+        echoTester("/foo",null);
+        echoTester("/foo/",null);
     }
 
-    public void echoTester(String path) throws Exception {
+    public void echoTester(String path, ClientEndpointConfig clientEndpointConfig)
+            throws Exception {
         WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer();
-        ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build();
+
+        if (clientEndpointConfig == null) {
+            clientEndpointConfig = ClientEndpointConfig.Builder.create().build();
+        }
         Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class,
                 clientEndpointConfig, new URI("ws://localhost:" + getPort() + path));
         CountDownLatch latch = new CountDownLatch(1);
@@ -120,4 +133,80 @@ public class TestWebSocketFrameClient ex
         wsSession.close();
     }
 
+    @Test
+    public void testConnectToBasicEndpoint() throws Exception {
+
+        Tomcat tomcat = getTomcatInstance();
+        Context ctx = tomcat.addContext(URI_PROTECTED, null);
+        ctx.addApplicationListener(TesterEchoServer.Config.class.getName());
+        Tomcat.addServlet(ctx, "default", new DefaultServlet());
+        ctx.addServletMappingDecoded("/", "default");
+
+        SecurityCollection collection = new SecurityCollection();
+        collection.addPatternDecoded("/");
+        String utf8User = "test";
+        String utf8Pass = "123£";
+
+        tomcat.addUser(utf8User, utf8Pass);
+        tomcat.addRole(utf8User, ROLE);
+
+        SecurityConstraint sc = new SecurityConstraint();
+        sc.addAuthRole(ROLE);
+        sc.addCollection(collection);
+        ctx.addConstraint(sc);
+
+        LoginConfig lc = new LoginConfig();
+        lc.setAuthMethod("BASIC");
+        ctx.setLoginConfig(lc);
+
+        AuthenticatorBase basicAuthenticator = new org.apache.catalina.authenticator.BasicAuthenticator();
+        ctx.getPipeline().addValve(basicAuthenticator);
+
+        tomcat.start();
+
+        ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build();
+        clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_USER_NAME, utf8User);
+        clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_PASSWORD, utf8Pass);
+
+        echoTester(URI_PROTECTED, clientEndpointConfig);
+
+    }
+
+    @Test
+    public void testConnectToDigestEndpoint() throws Exception {
+
+        Tomcat tomcat = getTomcatInstance();
+        Context ctx = tomcat.addContext(URI_PROTECTED, null);
+        ctx.addApplicationListener(TesterEchoServer.Config.class.getName());
+        Tomcat.addServlet(ctx, "default", new DefaultServlet());
+        ctx.addServletMappingDecoded("/", "default");
+
+        SecurityCollection collection = new SecurityCollection();
+        collection.addPatternDecoded("/*");
+
+        tomcat.addUser(USER, PWD);
+        tomcat.addRole(USER, ROLE);
+
+        SecurityConstraint sc = new SecurityConstraint();
+        sc.addAuthRole(ROLE);
+        sc.addCollection(collection);
+        ctx.addConstraint(sc);
+
+        LoginConfig lc = new LoginConfig();
+        lc.setAuthMethod("DIGEST");
+        ctx.setLoginConfig(lc);
+
+        AuthenticatorBase digestAuthenticator = new org.apache.catalina.authenticator.DigestAuthenticator();
+        ctx.getPipeline().addValve(digestAuthenticator);
+
+        tomcat.start();
+
+        ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build();
+        clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_USER_NAME, USER);
+        clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_PASSWORD,PWD);
+
+        echoTester(URI_PROTECTED, clientEndpointConfig);
+
+    }
+
 }

Modified: tomcat/trunk/webapps/docs/changelog.xml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1812129&r1=1812128&r2=1812129&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/changelog.xml (original)
+++ tomcat/trunk/webapps/docs/changelog.xml Fri Oct 13 13:42:25 2017
@@ -80,6 +80,14 @@
       </fix>
     </changelog>
   </subsection>
+  <subsection name="WebSocket">
+    <changelog>
+      <fix>
+        <bug>61604</bug>: Add support for authentication in the websocket
+        client. Patch submitted by J Fernandez. (remm)
+      </fix>
+    </changelog>
+  </subsection>
   <subsection name="Web applications">
     <changelog>
       <fix>



---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org