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