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