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-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to