This commit is contained in:
2024-11-30 19:03:49 +08:00
commit 1e6763c160
3806 changed files with 737676 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
/*
* 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.server;
/**
* Internal implementation constants.
*/
public class Constants {
public static final String BINARY_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM =
"org.apache.tomcat.websocket.binaryBufferSize";
public static final String TEXT_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM =
"org.apache.tomcat.websocket.textBufferSize";
public static final String ENFORCE_NO_ADD_AFTER_HANDSHAKE_CONTEXT_INIT_PARAM =
"org.apache.tomcat.websocket.noAddAfterHandshake";
public static final String SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE =
"javax.websocket.server.ServerContainer";
private Constants() {
// Hide default constructor
}
}

View File

@@ -0,0 +1,88 @@
/*
* 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.server;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.websocket.Extension;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
public class DefaultServerEndpointConfigurator
extends ServerEndpointConfig.Configurator {
@Override
public <T> T getEndpointInstance(Class<T> clazz)
throws InstantiationException {
try {
return clazz.getConstructor().newInstance();
} catch (InstantiationException e) {
throw e;
} catch (ReflectiveOperationException e) {
InstantiationException ie = new InstantiationException();
ie.initCause(e);
throw ie;
}
}
@Override
public String getNegotiatedSubprotocol(List<String> supported,
List<String> requested) {
for (String request : requested) {
if (supported.contains(request)) {
return request;
}
}
return "";
}
@Override
public List<Extension> getNegotiatedExtensions(List<Extension> installed,
List<Extension> requested) {
Set<String> installedNames = new HashSet<>();
for (Extension e : installed) {
installedNames.add(e.getName());
}
List<Extension> result = new ArrayList<>();
for (Extension request : requested) {
if (installedNames.contains(request.getName())) {
result.add(request);
}
}
return result;
}
@Override
public boolean checkOrigin(String originHeaderValue) {
return true;
}
@Override
public void modifyHandshake(ServerEndpointConfig sec,
HandshakeRequest request, HandshakeResponse response) {
// NO-OP
}
}

View File

@@ -0,0 +1,40 @@
# 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.
serverContainer.addNotAllowed=No further Endpoints may be registered once an attempt has been made to use one of the previously registered endpoints
serverContainer.configuratorFail=Failed to create configurator of type [{0}] for POJO of type [{1}]
serverContainer.duplicatePaths=Multiple Endpoints may not be deployed to the same path [{0}] : existing endpoint was [{1}] and new endpoint is [{2}]
serverContainer.encoderFail=Unable to create encoder of type [{0}]
serverContainer.failedDeployment=Deployment of WebSocket Endpoints to the web application with path [{0}] in host [{1}] is not permitted due to the failure of a previous deployment
serverContainer.missingAnnotation=Cannot deploy POJO class [{0}] as it is not annotated with @ServerEndpoint
serverContainer.servletContextMissing=No ServletContext was specified
upgradeUtil.incompatibleRsv=Extensions were specified that have incompatible RSV bit usage
uriTemplate.duplicateParameter=The parameter [{0}] appears more than once in the path which is not permitted
uriTemplate.emptySegment=The path [{0}] contains one or more empty segments which are is not permitted
uriTemplate.invalidPath=The path [{0}] is not valid.
uriTemplate.invalidSegment=The segment [{0}] is not valid in the provided path [{1}]
wsFrameServer.bytesRead=Read [{0}] bytes into input buffer ready for processing
wsFrameServer.illegalReadState=Unexpected read state [{0}]
wsFrameServer.onDataAvailable=Method entry
wsHttpUpgradeHandler.closeOnError=Closing WebSocket connection due to an error
wsHttpUpgradeHandler.destroyFailed=Failed to close WebConnection while destroying the WebSocket HttpUpgradeHandler
wsHttpUpgradeHandler.noPreInit=The preInit() method must be called to configure the WebSocket HttpUpgradeHandler before the container calls init(). Usually, this means the Servlet that created the WsHttpUpgradeHandler instance should also call preInit()
wsHttpUpgradeHandler.serverStop=The server is stopping
wsRemoteEndpointServer.closeFailed=Failed to close the ServletOutputStream connection cleanly

View File

@@ -0,0 +1,21 @@
# 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.
serverContainer.missingAnnotation=Die POJO Klasse [{0}] kann nicht deployed werden da sie nicht mit @ServerEndpoint annotiert ist.
serverContainer.servletContextMissing=Es wurde kein ServletContext angegeben
upgradeUtil.incompatibleRsv=Es wurden Erweiterungen spezifiziert, die eine inkompatible RSV Bit Konstellation erzeugen
uriTemplate.invalidPath=Der Pfad [{0}] ist nicht gültig.

View File

@@ -0,0 +1,20 @@
# 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.
serverContainer.configuratorFail=Fallo al crear configurador de tipo [{0}] para el POJO de tipo [{1}]
uriTemplate.invalidPath=El camino [{0}] no es válido.\n
wsFrameServer.bytesRead=[{0}] leídos del bufer de entrada llistos para ser procesados

View File

@@ -0,0 +1,40 @@
# 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.
serverContainer.addNotAllowed=Aucune terminaison ne peut être enregistré une fois qu'une tentative d'utilisation d'une des terminaisons précédemment enregistrée a été faite
serverContainer.configuratorFail=Echec de création du configurateur de type [{0}] pour le POJO de type [{1}]
serverContainer.duplicatePaths=Plusieurs terminaisons ne peuvent pas être déployés vers le même chemin [{0}]: la terminaison existante était [{1}] et la nouvelle est [{2}]
serverContainer.encoderFail=Impossible de créer un encodeur de type [{0}]
serverContainer.failedDeployment=Le déploiement de terminaisons WebSocket dans l''application web au chemin [{0}] dans l''hôte [{1}] n''est pas autorisé à cause de l''échec lors d''un précédent déploiement
serverContainer.missingAnnotation=Impossible de déployer la classe POJO [{0}] car elle n''a pas été annotée avec @ServerEndpoint
serverContainer.servletContextMissing=Aucun ServletContext n'a été spécifié
upgradeUtil.incompatibleRsv=Des extensions qui ont été spécifiées ont une utilisation incompatible du bit RSV
uriTemplate.duplicateParameter=Le paramètre [{0}] apparaît plus d''une fois dans le chemin ce qui n''est pas permis
uriTemplate.emptySegment=Le chemin [{0}] contient un ou plusieurs segments vide ce qui n''est pas autorisé
uriTemplate.invalidPath=Le chemin [{0}] est invalide
uriTemplate.invalidSegment=Le segment [{0}] est invalide pour le chemin fourni [{1}]
wsFrameServer.bytesRead=Lu [{0}] octets dans le buffer de réception prêts à être traités
wsFrameServer.illegalReadState=Etat de lecture inattendu [{0}]
wsFrameServer.onDataAvailable=Entrée de méthode
wsHttpUpgradeHandler.closeOnError=Fermeture de la connection WebSocket à cause d'une erreur
wsHttpUpgradeHandler.destroyFailed=Echec de la fermeture de la WebConnection lors de la destruction du HttpUpgradeHandler de WebSocket
wsHttpUpgradeHandler.noPreInit=La méthode preInit() doit être appelée pour configurer le HttpUpgradeHandler de Websockets avant que le container n'appelle init(), cela veut habituellement dire que le Servlet qui a crée l'instance du WsHttpUpgradeHandler doit aussi appeler preInit()
wsHttpUpgradeHandler.serverStop=Le serveur est en train de s'arrêter
wsRemoteEndpointServer.closeFailed=Impossible de fermer le ServletOutputStream proprement

View File

@@ -0,0 +1,40 @@
# 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.
serverContainer.addNotAllowed=以前に登録したことのあるエンドポイントには登録できません。
serverContainer.configuratorFail=POJO クラス [{1}] のインスタンスを構成するクラス [{0}] のインスタンスを作成できません。
serverContainer.duplicatePaths=複数のエンドポイントを同じパス[{0}]に配備することはできません:既存のエンドポイントは[{1}]で、新しいエンドポイントは[{2}]です。
serverContainer.encoderFail=タイプ[{0}]のエンコーダを作成できません
serverContainer.failedDeployment=以前のデプロイメントが失敗したため、ホスト[{1}]内のパス[{0}]を持つWebアプリケーションへのWebSocketエンドポイントのデプロイメントは許可されていません。
serverContainer.missingAnnotation=POJOクラス[{0}]は@ServerEndpointでアテーション付けされていないため、デプロイ出来ません。
serverContainer.servletContextMissing=ServletContextが指定されていません
upgradeUtil.incompatibleRsv=互換性のないRSVビットの使用法を持つ拡張が指定されました
uriTemplate.duplicateParameter=パス中にパラメーター [{0}] を複数回登場させることはできません。
uriTemplate.emptySegment=パス [{0}] に一つ以上の空セグメントを含めることはできません。
uriTemplate.invalidPath=[{0}] は不正なパスです。
uriTemplate.invalidSegment=パス [{1}] に存在しないセグメント [{0}] が指定されました。
wsFrameServer.bytesRead=入力バッファーに読み込んだ [{0}] バイトのデータは処理可能です。
wsFrameServer.illegalReadState=予期しない読み取り状態[{0}]
wsFrameServer.onDataAvailable=メソッドエントリ
wsHttpUpgradeHandler.closeOnError=エラーが発生したため WebSocket コネクションを切断します。
wsHttpUpgradeHandler.destroyFailed=WebSocket HttpUpgradeHandlerを破棄している間にWebConnectionを閉じることができませんでした。
wsHttpUpgradeHandler.noPreInit=コンテナがinit()を呼び出す前に、preInit()メソッドを呼び出すようにWebSocket HttpUpgradeHandlerを設定する必要があります。 通常、これはWsHttpUpgradeHandlerインスタンスを作成したサーブレットがpreInit()を呼び出す必要があることを意味します。
wsHttpUpgradeHandler.serverStop=サーバ停止中
wsRemoteEndpointServer.closeFailed=ServletOutputStreamコネクションを正常に閉じることができませんでした

View File

@@ -0,0 +1,40 @@
# 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.
serverContainer.addNotAllowed=이전에 등록된 엔드포인트들 중 하나라도 사용하려고 시도하게 되면, 엔드포인트들을 더 이상 등록할 수 없습니다.
serverContainer.configuratorFail=타입이 [{1}]인 POJO를 위한 타입 [{0}]의 Configurator를 생성하지 못했습니다.
serverContainer.duplicatePaths=여러 개의 엔드포인트들이 동일한 경로 [{0}]에 배치될 수 없습니다: 기존 엔드포인트는 [{1}]였으며 신규 엔드포인트는 [{2}]입니다.
serverContainer.encoderFail=타입이 [{0}]인 인코더를 생성할 수 없습니다.
serverContainer.failedDeployment=이전 배치의 실패로 인하여, 호스트 [{1}] 내에 경로 [{0}]의 웹 애플리케이션에 대한 웹소켓 엔드포인트들의 배치가 허용되지 않습니다.
serverContainer.missingAnnotation=클래스가 @ServerEndpoint로 annotate되어 있지 않기에, POJO 클래스 [{0}]을(를) 배치할 수 없습니다.
serverContainer.servletContextMissing=지정된 ServletContext가 없습니다.
upgradeUtil.incompatibleRsv=호환되지 않는 RSV 비트를 사용하여, Extension들이 지정되었습니다.
uriTemplate.duplicateParameter=허용되지 않는 경로에서, 파라미터 [{0}]이(가) 두번 이상 나타나고 있습니다.
uriTemplate.emptySegment=경로 [{0}]이(가), 하나 이상의 허용되지 않는 empty segment들을 포함하고 있습니다.
uriTemplate.invalidPath=경로 [{0}](은)는 유효하지 않습니다.
uriTemplate.invalidSegment=세그먼트 [{0}]은(는) 제공된 경로 [{1}] 내에 유효하지 않습니다.
wsFrameServer.bytesRead=[{0}] 바이트를 입력 버퍼에 읽어 처리를 준비합니다.
wsFrameServer.illegalReadState=예기치 않은 읽기 상태 [{0}]
wsFrameServer.onDataAvailable=메소드 엔트리
wsHttpUpgradeHandler.closeOnError=오류 발생으로 인하여, 웹소켓 연결을 닫습니다.
wsHttpUpgradeHandler.destroyFailed=웹소켓 HttpUpgradeHandler를 소멸시키는 중, WebConnection을 닫지 못했습니다.
wsHttpUpgradeHandler.noPreInit=컨테이너가 init()을 호출하기 전에 웹소켓 HttpUpgradeHandler를 설정하기 위하여, preInit() 메소드가 반드시 호출되어야만 합니다. 통상 이는 WsHttpUpgradeHandler 인스턴스를 생성한 서블릿도 preInit()을 호출해야 함을 의미합니다.
wsHttpUpgradeHandler.serverStop=서버가 중지되고 있는 중입니다.
wsRemoteEndpointServer.closeFailed=해당 ServletOutputStream의 연결을 깨끗하게 닫지 못했습니다.

View File

@@ -0,0 +1,31 @@
# 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.
serverContainer.configuratorFail=无法为[{1}]类型的POJO创建类型[{0}]的配置程序
serverContainer.duplicatePaths=多个端点可能不能发不到同一个路径[{0}]:已经存在的端点[{1}]和新的端点[{2}]
serverContainer.encoderFail=无法创建[{0}]类型的编码器
serverContainer.failedDeployment=由于以前的部署失败不允许将WebSocket终结点部署到主机[{1}]中路径为[{0}]的Web应用程序
serverContainer.servletContextMissing=没有指定ServletContext
upgradeUtil.incompatibleRsv=指定扩展名具有不兼容的RSV位使用
uriTemplate.invalidPath=路径 [{0}] 无效。
wsFrameServer.bytesRead=将[{0}]个字节读入输入缓冲区,准备进行处理
wsFrameServer.onDataAvailable=进入方法
wsHttpUpgradeHandler.noPreInit=在容器调用init()之前必须调用preinit()方法来配置WebSocket HttpUpgradeHandler。通常这意味着创建WsHttpUpgradeHandler 实例的servlet也应该调用preinit()
wsRemoteEndpointServer.closeFailed=无法完全关闭ServletOutputStream 连接

View File

@@ -0,0 +1,339 @@
/*
* 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.server;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.websocket.Endpoint;
import javax.websocket.Extension;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.ServerEndpointConfig;
import org.apache.tomcat.util.codec.binary.Base64;
import org.apache.tomcat.util.res.StringManager;
import org.apache.tomcat.util.security.ConcurrentMessageDigest;
import org.apache.tomcat.websocket.Constants;
import org.apache.tomcat.websocket.Transformation;
import org.apache.tomcat.websocket.TransformationFactory;
import org.apache.tomcat.websocket.Util;
import org.apache.tomcat.websocket.WsHandshakeResponse;
import org.apache.tomcat.websocket.pojo.PojoEndpointServer;
public class UpgradeUtil {
private static final StringManager sm =
StringManager.getManager(UpgradeUtil.class.getPackage().getName());
private static final byte[] WS_ACCEPT =
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11".getBytes(
StandardCharsets.ISO_8859_1);
private UpgradeUtil() {
// Utility class. Hide default constructor.
}
/**
* Checks to see if this is an HTTP request that includes a valid upgrade
* request to web socket.
* <p>
* Note: RFC 2616 does not limit HTTP upgrade to GET requests but the Java
* WebSocket spec 1.0, section 8.2 implies such a limitation and RFC
* 6455 section 4.1 requires that a WebSocket Upgrade uses GET.
* @param request The request to check if it is an HTTP upgrade request for
* a WebSocket connection
* @param response The response associated with the request
* @return <code>true</code> if the request includes an HTTP Upgrade request
* for the WebSocket protocol, otherwise <code>false</code>
*/
public static boolean isWebSocketUpgradeRequest(ServletRequest request,
ServletResponse response) {
return ((request instanceof HttpServletRequest) &&
(response instanceof HttpServletResponse) &&
headerContainsToken((HttpServletRequest) request,
Constants.UPGRADE_HEADER_NAME,
Constants.UPGRADE_HEADER_VALUE) &&
"GET".equals(((HttpServletRequest) request).getMethod()));
}
public static void doUpgrade(WsServerContainer sc, HttpServletRequest req,
HttpServletResponse resp, ServerEndpointConfig sec,
Map<String,String> pathParams)
throws ServletException, IOException {
// Validate the rest of the headers and reject the request if that
// validation fails
String key;
String subProtocol = null;
if (!headerContainsToken(req, Constants.CONNECTION_HEADER_NAME,
Constants.CONNECTION_HEADER_VALUE)) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
if (!headerContainsToken(req, Constants.WS_VERSION_HEADER_NAME,
Constants.WS_VERSION_HEADER_VALUE)) {
resp.setStatus(426);
resp.setHeader(Constants.WS_VERSION_HEADER_NAME,
Constants.WS_VERSION_HEADER_VALUE);
return;
}
key = req.getHeader(Constants.WS_KEY_HEADER_NAME);
if (key == null) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
// Origin check
String origin = req.getHeader(Constants.ORIGIN_HEADER_NAME);
if (!sec.getConfigurator().checkOrigin(origin)) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
// Sub-protocols
List<String> subProtocols = getTokensFromHeader(req,
Constants.WS_PROTOCOL_HEADER_NAME);
subProtocol = sec.getConfigurator().getNegotiatedSubprotocol(
sec.getSubprotocols(), subProtocols);
// Extensions
// Should normally only be one header but handle the case of multiple
// headers
List<Extension> extensionsRequested = new ArrayList<>();
Enumeration<String> extHeaders = req.getHeaders(Constants.WS_EXTENSIONS_HEADER_NAME);
while (extHeaders.hasMoreElements()) {
Util.parseExtensionHeader(extensionsRequested, extHeaders.nextElement());
}
// Negotiation phase 1. By default this simply filters out the
// extensions that the server does not support but applications could
// use a custom configurator to do more than this.
List<Extension> installedExtensions = null;
if (sec.getExtensions().size() == 0) {
installedExtensions = Constants.INSTALLED_EXTENSIONS;
} else {
installedExtensions = new ArrayList<>();
installedExtensions.addAll(sec.getExtensions());
installedExtensions.addAll(Constants.INSTALLED_EXTENSIONS);
}
List<Extension> negotiatedExtensionsPhase1 = sec.getConfigurator().getNegotiatedExtensions(
installedExtensions, extensionsRequested);
// Negotiation phase 2. Create the Transformations that will be applied
// to this connection. Note than an extension may be dropped at this
// point if the client has requested a configuration that the server is
// unable to support.
List<Transformation> transformations = createTransformations(negotiatedExtensionsPhase1);
List<Extension> negotiatedExtensionsPhase2;
if (transformations.isEmpty()) {
negotiatedExtensionsPhase2 = Collections.emptyList();
} else {
negotiatedExtensionsPhase2 = new ArrayList<>(transformations.size());
for (Transformation t : transformations) {
negotiatedExtensionsPhase2.add(t.getExtensionResponse());
}
}
// Build the transformation pipeline
Transformation transformation = null;
StringBuilder responseHeaderExtensions = new StringBuilder();
boolean first = true;
for (Transformation t : transformations) {
if (first) {
first = false;
} else {
responseHeaderExtensions.append(',');
}
append(responseHeaderExtensions, t.getExtensionResponse());
if (transformation == null) {
transformation = t;
} else {
transformation.setNext(t);
}
}
// Now we have the full pipeline, validate the use of the RSV bits.
if (transformation != null && !transformation.validateRsvBits(0)) {
throw new ServletException(sm.getString("upgradeUtil.incompatibleRsv"));
}
// If we got this far, all is good. Accept the connection.
resp.setHeader(Constants.UPGRADE_HEADER_NAME,
Constants.UPGRADE_HEADER_VALUE);
resp.setHeader(Constants.CONNECTION_HEADER_NAME,
Constants.CONNECTION_HEADER_VALUE);
resp.setHeader(HandshakeResponse.SEC_WEBSOCKET_ACCEPT,
getWebSocketAccept(key));
if (subProtocol != null && subProtocol.length() > 0) {
// RFC6455 4.2.2 explicitly states "" is not valid here
resp.setHeader(Constants.WS_PROTOCOL_HEADER_NAME, subProtocol);
}
if (!transformations.isEmpty()) {
resp.setHeader(Constants.WS_EXTENSIONS_HEADER_NAME, responseHeaderExtensions.toString());
}
WsHandshakeRequest wsRequest = new WsHandshakeRequest(req, pathParams);
WsHandshakeResponse wsResponse = new WsHandshakeResponse();
WsPerSessionServerEndpointConfig perSessionServerEndpointConfig =
new WsPerSessionServerEndpointConfig(sec);
sec.getConfigurator().modifyHandshake(perSessionServerEndpointConfig,
wsRequest, wsResponse);
wsRequest.finished();
// Add any additional headers
for (Entry<String,List<String>> entry :
wsResponse.getHeaders().entrySet()) {
for (String headerValue: entry.getValue()) {
resp.addHeader(entry.getKey(), headerValue);
}
}
Endpoint ep;
try {
Class<?> clazz = sec.getEndpointClass();
if (Endpoint.class.isAssignableFrom(clazz)) {
ep = (Endpoint) sec.getConfigurator().getEndpointInstance(
clazz);
} else {
ep = new PojoEndpointServer();
// Need to make path params available to POJO
perSessionServerEndpointConfig.getUserProperties().put(
org.apache.tomcat.websocket.pojo.Constants.POJO_PATH_PARAM_KEY, pathParams);
}
} catch (InstantiationException e) {
throw new ServletException(e);
}
WsHttpUpgradeHandler wsHandler =
req.upgrade(WsHttpUpgradeHandler.class);
wsHandler.preInit(ep, perSessionServerEndpointConfig, sc, wsRequest,
negotiatedExtensionsPhase2, subProtocol, transformation, pathParams,
req.isSecure());
}
private static List<Transformation> createTransformations(
List<Extension> negotiatedExtensions) {
TransformationFactory factory = TransformationFactory.getInstance();
LinkedHashMap<String,List<List<Extension.Parameter>>> extensionPreferences =
new LinkedHashMap<>();
// Result will likely be smaller than this
List<Transformation> result = new ArrayList<>(negotiatedExtensions.size());
for (Extension extension : negotiatedExtensions) {
List<List<Extension.Parameter>> preferences =
extensionPreferences.get(extension.getName());
if (preferences == null) {
preferences = new ArrayList<>();
extensionPreferences.put(extension.getName(), preferences);
}
preferences.add(extension.getParameters());
}
for (Map.Entry<String,List<List<Extension.Parameter>>> entry :
extensionPreferences.entrySet()) {
Transformation transformation = factory.create(entry.getKey(), entry.getValue(), true);
if (transformation != null) {
result.add(transformation);
}
}
return result;
}
private static void append(StringBuilder sb, Extension extension) {
if (extension == null || extension.getName() == null || extension.getName().length() == 0) {
return;
}
sb.append(extension.getName());
for (Extension.Parameter p : extension.getParameters()) {
sb.append(';');
sb.append(p.getName());
if (p.getValue() != null) {
sb.append('=');
sb.append(p.getValue());
}
}
}
/*
* This only works for tokens. Quoted strings need more sophisticated
* parsing.
*/
private static boolean headerContainsToken(HttpServletRequest req,
String headerName, String target) {
Enumeration<String> headers = req.getHeaders(headerName);
while (headers.hasMoreElements()) {
String header = headers.nextElement();
String[] tokens = header.split(",");
for (String token : tokens) {
if (target.equalsIgnoreCase(token.trim())) {
return true;
}
}
}
return false;
}
/*
* This only works for tokens. Quoted strings need more sophisticated
* parsing.
*/
private static List<String> getTokensFromHeader(HttpServletRequest req,
String headerName) {
List<String> result = new ArrayList<>();
Enumeration<String> headers = req.getHeaders(headerName);
while (headers.hasMoreElements()) {
String header = headers.nextElement();
String[] tokens = header.split(",");
for (String token : tokens) {
result.add(token.trim());
}
}
return result;
}
private static String getWebSocketAccept(String key) {
byte[] digest = ConcurrentMessageDigest.digestSHA1(
key.getBytes(StandardCharsets.ISO_8859_1), WS_ACCEPT);
return Base64.encodeBase64String(digest);
}
}

View File

@@ -0,0 +1,177 @@
/*
* 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.server;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.websocket.DeploymentException;
import org.apache.tomcat.util.res.StringManager;
/**
* Extracts path parameters from URIs used to create web socket connections
* using the URI template defined for the associated Endpoint.
*/
public class UriTemplate {
private static final StringManager sm = StringManager.getManager(UriTemplate.class);
private final String normalized;
private final List<Segment> segments = new ArrayList<>();
private final boolean hasParameters;
public UriTemplate(String path) throws DeploymentException {
if (path == null || path.length() ==0 || !path.startsWith("/")) {
throw new DeploymentException(
sm.getString("uriTemplate.invalidPath", path));
}
StringBuilder normalized = new StringBuilder(path.length());
Set<String> paramNames = new HashSet<>();
// Include empty segments.
String[] segments = path.split("/", -1);
int paramCount = 0;
int segmentCount = 0;
for (int i = 0; i < segments.length; i++) {
String segment = segments[i];
if (segment.length() == 0) {
if (i == 0 || (i == segments.length - 1 && paramCount == 0)) {
// Ignore the first empty segment as the path must always
// start with '/'
// Ending with a '/' is also OK for instances used for
// matches but not for parameterised templates.
continue;
} else {
// As per EG discussion, all other empty segments are
// invalid
throw new IllegalArgumentException(sm.getString(
"uriTemplate.emptySegment", path));
}
}
normalized.append('/');
int index = -1;
if (segment.startsWith("{") && segment.endsWith("}")) {
index = segmentCount;
segment = segment.substring(1, segment.length() - 1);
normalized.append('{');
normalized.append(paramCount++);
normalized.append('}');
if (!paramNames.add(segment)) {
throw new IllegalArgumentException(sm.getString(
"uriTemplate.duplicateParameter", segment));
}
} else {
if (segment.contains("{") || segment.contains("}")) {
throw new IllegalArgumentException(sm.getString(
"uriTemplate.invalidSegment", segment, path));
}
normalized.append(segment);
}
this.segments.add(new Segment(index, segment));
segmentCount++;
}
this.normalized = normalized.toString();
this.hasParameters = paramCount > 0;
}
public Map<String,String> match(UriTemplate candidate) {
Map<String,String> result = new HashMap<>();
// Should not happen but for safety
if (candidate.getSegmentCount() != getSegmentCount()) {
return null;
}
Iterator<Segment> candidateSegments =
candidate.getSegments().iterator();
Iterator<Segment> targetSegments = segments.iterator();
while (candidateSegments.hasNext()) {
Segment candidateSegment = candidateSegments.next();
Segment targetSegment = targetSegments.next();
if (targetSegment.getParameterIndex() == -1) {
// Not a parameter - values must match
if (!targetSegment.getValue().equals(
candidateSegment.getValue())) {
// Not a match. Stop here
return null;
}
} else {
// Parameter
result.put(targetSegment.getValue(),
candidateSegment.getValue());
}
}
return result;
}
public boolean hasParameters() {
return hasParameters;
}
public int getSegmentCount() {
return segments.size();
}
public String getNormalizedPath() {
return normalized;
}
private List<Segment> getSegments() {
return segments;
}
private static class Segment {
private final int parameterIndex;
private final String value;
public Segment(int parameterIndex, String value) {
this.parameterIndex = parameterIndex;
this.value = value;
}
public int getParameterIndex() {
return parameterIndex;
}
public String getValue() {
return value;
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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.server;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
/**
* In normal usage, this {@link ServletContextListener} does not need to be
* explicitly configured as the {@link WsSci} performs all the necessary
* bootstrap and installs this listener in the {@link ServletContext}. If the
* {@link WsSci} is disabled, this listener must be added manually to every
* {@link ServletContext} that uses WebSocket to bootstrap the
* {@link WsServerContainer} correctly.
*/
public class WsContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext sc = sce.getServletContext();
// Don't trigger WebSocket initialization if a WebSocket Server
// Container is already present
if (sc.getAttribute(Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE) == null) {
WsSci.init(sce.getServletContext(), false);
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
ServletContext sc = sce.getServletContext();
Object obj = sc.getAttribute(Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE);
if (obj instanceof WsServerContainer) {
((WsServerContainer) obj).destroy();
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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.server;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Handles the initial HTTP connection for WebSocket connections.
*/
public class WsFilter implements Filter {
private WsServerContainer sc;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
sc = (WsServerContainer) filterConfig.getServletContext().getAttribute(
Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// This filter only needs to handle WebSocket upgrade requests
if (!sc.areEndpointsRegistered() ||
!UpgradeUtil.isWebSocketUpgradeRequest(request, response)) {
chain.doFilter(request, response);
return;
}
// HTTP request with an upgrade header for WebSocket present
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// Check to see if this WebSocket implementation has a matching mapping
String path;
String pathInfo = req.getPathInfo();
if (pathInfo == null) {
path = req.getServletPath();
} else {
path = req.getServletPath() + pathInfo;
}
WsMappingResult mappingResult = sc.findMapping(path);
if (mappingResult == null) {
// No endpoint registered for the requested path. Let the
// application handle it (it might redirect or forward for example)
chain.doFilter(request, response);
return;
}
UpgradeUtil.doUpgrade(sc, req, resp, mappingResult.getConfig(),
mappingResult.getPathParams());
}
@Override
public void destroy() {
// NO-OP
}
}

View File

@@ -0,0 +1,192 @@
/*
* 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.server;
import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState;
import org.apache.tomcat.util.net.SocketEvent;
import org.apache.tomcat.util.net.SocketWrapperBase;
import org.apache.tomcat.util.res.StringManager;
import org.apache.tomcat.websocket.Transformation;
import org.apache.tomcat.websocket.WsFrameBase;
import org.apache.tomcat.websocket.WsIOException;
import org.apache.tomcat.websocket.WsSession;
public class WsFrameServer extends WsFrameBase {
private final Log log = LogFactory.getLog(WsFrameServer.class); // must not be static
private static final StringManager sm = StringManager.getManager(WsFrameServer.class);
private final SocketWrapperBase<?> socketWrapper;
private final ClassLoader applicationClassLoader;
public WsFrameServer(SocketWrapperBase<?> socketWrapper, WsSession wsSession,
Transformation transformation, ClassLoader applicationClassLoader) {
super(wsSession, transformation);
this.socketWrapper = socketWrapper;
this.applicationClassLoader = applicationClassLoader;
}
/**
* Called when there is data in the ServletInputStream to process.
*
* @throws IOException if an I/O error occurs while processing the available
* data
*/
private void onDataAvailable() throws IOException {
if (log.isDebugEnabled()) {
log.debug("wsFrameServer.onDataAvailable");
}
if (isOpen() && inputBuffer.hasRemaining() && !isSuspended()) {
// There might be a data that was left in the buffer when
// the read has been suspended.
// Consume this data before reading from the socket.
processInputBuffer();
}
while (isOpen() && !isSuspended()) {
// Fill up the input buffer with as much data as we can
inputBuffer.mark();
inputBuffer.position(inputBuffer.limit()).limit(inputBuffer.capacity());
int read = socketWrapper.read(false, inputBuffer);
inputBuffer.limit(inputBuffer.position()).reset();
if (read < 0) {
throw new EOFException();
} else if (read == 0) {
return;
}
if (log.isDebugEnabled()) {
log.debug(sm.getString("wsFrameServer.bytesRead", Integer.toString(read)));
}
processInputBuffer();
}
}
@Override
protected boolean isMasked() {
// Data is from the client so it should be masked
return true;
}
@Override
protected Transformation getTransformation() {
// Overridden to make it visible to other classes in this package
return super.getTransformation();
}
@Override
protected boolean isOpen() {
// Overridden to make it visible to other classes in this package
return super.isOpen();
}
@Override
protected Log getLog() {
return log;
}
@Override
protected void sendMessageText(boolean last) throws WsIOException {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(applicationClassLoader);
super.sendMessageText(last);
} finally {
Thread.currentThread().setContextClassLoader(cl);
}
}
@Override
protected void sendMessageBinary(ByteBuffer msg, boolean last) throws WsIOException {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(applicationClassLoader);
super.sendMessageBinary(msg, last);
} finally {
Thread.currentThread().setContextClassLoader(cl);
}
}
@Override
protected void resumeProcessing() {
socketWrapper.processSocket(SocketEvent.OPEN_READ, true);
}
SocketState notifyDataAvailable() throws IOException {
while (isOpen()) {
switch (getReadState()) {
case WAITING:
if (!changeReadState(ReadState.WAITING, ReadState.PROCESSING)) {
continue;
}
try {
return doOnDataAvailable();
} catch (IOException e) {
changeReadState(ReadState.CLOSING);
throw e;
}
case SUSPENDING_WAIT:
if (!changeReadState(ReadState.SUSPENDING_WAIT, ReadState.SUSPENDED)) {
continue;
}
return SocketState.SUSPENDED;
default:
throw new IllegalStateException(
sm.getString("wsFrameServer.illegalReadState", getReadState()));
}
}
return SocketState.CLOSED;
}
private SocketState doOnDataAvailable() throws IOException {
onDataAvailable();
while (isOpen()) {
switch (getReadState()) {
case PROCESSING:
if (!changeReadState(ReadState.PROCESSING, ReadState.WAITING)) {
continue;
}
return SocketState.UPGRADED;
case SUSPENDING_PROCESS:
if (!changeReadState(ReadState.SUSPENDING_PROCESS, ReadState.SUSPENDED)) {
continue;
}
return SocketState.SUSPENDED;
default:
throw new IllegalStateException(
sm.getString("wsFrameServer.illegalReadState", getReadState()));
}
}
return SocketState.CLOSED;
}
}

View File

@@ -0,0 +1,189 @@
/*
* 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.server;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.Principal;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.http.HttpServletRequest;
import javax.websocket.server.HandshakeRequest;
import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap;
import org.apache.tomcat.util.res.StringManager;
/**
* Represents the request that this session was opened under.
*/
public class WsHandshakeRequest implements HandshakeRequest {
private static final StringManager sm = StringManager.getManager(WsHandshakeRequest.class);
private final URI requestUri;
private final Map<String,List<String>> parameterMap;
private final String queryString;
private final Principal userPrincipal;
private final Map<String,List<String>> headers;
private final Object httpSession;
private volatile HttpServletRequest request;
public WsHandshakeRequest(HttpServletRequest request, Map<String,String> pathParams) {
this.request = request;
queryString = request.getQueryString();
userPrincipal = request.getUserPrincipal();
httpSession = request.getSession(false);
requestUri = buildRequestUri(request);
// ParameterMap
Map<String,String[]> originalParameters = request.getParameterMap();
Map<String,List<String>> newParameters =
new HashMap<>(originalParameters.size());
for (Entry<String,String[]> entry : originalParameters.entrySet()) {
newParameters.put(entry.getKey(),
Collections.unmodifiableList(
Arrays.asList(entry.getValue())));
}
for (Entry<String,String> entry : pathParams.entrySet()) {
newParameters.put(entry.getKey(), Collections.singletonList(entry.getValue()));
}
parameterMap = Collections.unmodifiableMap(newParameters);
// Headers
Map<String,List<String>> newHeaders = new CaseInsensitiveKeyMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
newHeaders.put(headerName, Collections.unmodifiableList(
Collections.list(request.getHeaders(headerName))));
}
headers = Collections.unmodifiableMap(newHeaders);
}
@Override
public URI getRequestURI() {
return requestUri;
}
@Override
public Map<String,List<String>> getParameterMap() {
return parameterMap;
}
@Override
public String getQueryString() {
return queryString;
}
@Override
public Principal getUserPrincipal() {
return userPrincipal;
}
@Override
public Map<String,List<String>> getHeaders() {
return headers;
}
@Override
public boolean isUserInRole(String role) {
if (request == null) {
throw new IllegalStateException();
}
return request.isUserInRole(role);
}
@Override
public Object getHttpSession() {
return httpSession;
}
/**
* Called when the HandshakeRequest is no longer required. Since an instance
* of this class retains a reference to the current HttpServletRequest that
* reference needs to be cleared as the HttpServletRequest may be reused.
*
* There is no reason for instances of this class to be accessed once the
* handshake has been completed.
*/
void finished() {
request = null;
}
/*
* See RequestUtil.getRequestURL()
*/
private static URI buildRequestUri(HttpServletRequest req) {
StringBuffer uri = new StringBuffer();
String scheme = req.getScheme();
int port = req.getServerPort();
if (port < 0) {
// Work around java.net.URL bug
port = 80;
}
if ("http".equals(scheme)) {
uri.append("ws");
} else if ("https".equals(scheme)) {
uri.append("wss");
} else {
// Should never happen
throw new IllegalArgumentException(
sm.getString("wsHandshakeRequest.unknownScheme", scheme));
}
uri.append("://");
uri.append(req.getServerName());
if ((scheme.equals("http") && (port != 80))
|| (scheme.equals("https") && (port != 443))) {
uri.append(':');
uri.append(port);
}
uri.append(req.getRequestURI());
if (req.getQueryString() != null) {
uri.append("?");
uri.append(req.getQueryString());
}
try {
return new URI(uri.toString());
} catch (URISyntaxException e) {
// Should never happen
throw new IllegalArgumentException(
sm.getString("wsHandshakeRequest.invalidUri", uri.toString()), e);
}
}
}

View File

@@ -0,0 +1,248 @@
/*
* 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.server;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpSession;
import javax.servlet.http.WebConnection;
import javax.websocket.CloseReason;
import javax.websocket.CloseReason.CloseCodes;
import javax.websocket.DeploymentException;
import javax.websocket.Endpoint;
import javax.websocket.Extension;
import javax.websocket.server.ServerEndpointConfig;
import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState;
import org.apache.tomcat.util.net.SSLSupport;
import org.apache.tomcat.util.net.SocketEvent;
import org.apache.tomcat.util.net.SocketWrapperBase;
import org.apache.tomcat.util.res.StringManager;
import org.apache.tomcat.websocket.Transformation;
import org.apache.tomcat.websocket.WsIOException;
import org.apache.tomcat.websocket.WsSession;
/**
* Servlet 3.1 HTTP upgrade handler for WebSocket connections.
*/
public class WsHttpUpgradeHandler implements InternalHttpUpgradeHandler {
private final Log log = LogFactory.getLog(WsHttpUpgradeHandler.class); // must not be static
private static final StringManager sm = StringManager.getManager(WsHttpUpgradeHandler.class);
private final ClassLoader applicationClassLoader;
private SocketWrapperBase<?> socketWrapper;
private Endpoint ep;
private ServerEndpointConfig serverEndpointConfig;
private WsServerContainer webSocketContainer;
private WsHandshakeRequest handshakeRequest;
private List<Extension> negotiatedExtensions;
private String subProtocol;
private Transformation transformation;
private Map<String,String> pathParameters;
private boolean secure;
private WebConnection connection;
private WsRemoteEndpointImplServer wsRemoteEndpointServer;
private WsFrameServer wsFrame;
private WsSession wsSession;
public WsHttpUpgradeHandler() {
applicationClassLoader = Thread.currentThread().getContextClassLoader();
}
@Override
public void setSocketWrapper(SocketWrapperBase<?> socketWrapper) {
this.socketWrapper = socketWrapper;
}
public void preInit(Endpoint ep, ServerEndpointConfig serverEndpointConfig,
WsServerContainer wsc, WsHandshakeRequest handshakeRequest,
List<Extension> negotiatedExtensionsPhase2, String subProtocol,
Transformation transformation, Map<String,String> pathParameters,
boolean secure) {
this.ep = ep;
this.serverEndpointConfig = serverEndpointConfig;
this.webSocketContainer = wsc;
this.handshakeRequest = handshakeRequest;
this.negotiatedExtensions = negotiatedExtensionsPhase2;
this.subProtocol = subProtocol;
this.transformation = transformation;
this.pathParameters = pathParameters;
this.secure = secure;
}
@Override
public void init(WebConnection connection) {
if (ep == null) {
throw new IllegalStateException(
sm.getString("wsHttpUpgradeHandler.noPreInit"));
}
String httpSessionId = null;
Object session = handshakeRequest.getHttpSession();
if (session != null ) {
httpSessionId = ((HttpSession) session).getId();
}
// Need to call onOpen using the web application's class loader
// Create the frame using the application's class loader so it can pick
// up application specific config from the ServerContainerImpl
Thread t = Thread.currentThread();
ClassLoader cl = t.getContextClassLoader();
t.setContextClassLoader(applicationClassLoader);
try {
wsRemoteEndpointServer = new WsRemoteEndpointImplServer(socketWrapper, webSocketContainer);
wsSession = new WsSession(ep, wsRemoteEndpointServer,
webSocketContainer, handshakeRequest.getRequestURI(),
handshakeRequest.getParameterMap(),
handshakeRequest.getQueryString(),
handshakeRequest.getUserPrincipal(), httpSessionId,
negotiatedExtensions, subProtocol, pathParameters, secure,
serverEndpointConfig);
wsFrame = new WsFrameServer(socketWrapper, wsSession, transformation,
applicationClassLoader);
// WsFrame adds the necessary final transformations. Copy the
// completed transformation chain to the remote end point.
wsRemoteEndpointServer.setTransformation(wsFrame.getTransformation());
ep.onOpen(wsSession, serverEndpointConfig);
webSocketContainer.registerSession(serverEndpointConfig.getPath(), wsSession);
} catch (DeploymentException e) {
throw new IllegalArgumentException(e);
} finally {
t.setContextClassLoader(cl);
}
}
@Override
public SocketState upgradeDispatch(SocketEvent status) {
switch (status) {
case OPEN_READ:
try {
return wsFrame.notifyDataAvailable();
} catch (WsIOException ws) {
close(ws.getCloseReason());
} catch (IOException ioe) {
onError(ioe);
CloseReason cr = new CloseReason(
CloseCodes.CLOSED_ABNORMALLY, ioe.getMessage());
close(cr);
}
return SocketState.CLOSED;
case OPEN_WRITE:
wsRemoteEndpointServer.onWritePossible(false);
break;
case STOP:
CloseReason cr = new CloseReason(CloseCodes.GOING_AWAY,
sm.getString("wsHttpUpgradeHandler.serverStop"));
try {
wsSession.close(cr);
} catch (IOException ioe) {
onError(ioe);
cr = new CloseReason(
CloseCodes.CLOSED_ABNORMALLY, ioe.getMessage());
close(cr);
return SocketState.CLOSED;
}
break;
case ERROR:
String msg = sm.getString("wsHttpUpgradeHandler.closeOnError");
wsSession.doClose(new CloseReason(CloseCodes.GOING_AWAY, msg),
new CloseReason(CloseCodes.CLOSED_ABNORMALLY, msg));
//$FALL-THROUGH$
case DISCONNECT:
case TIMEOUT:
case CONNECT_FAIL:
return SocketState.CLOSED;
}
if (wsFrame.isOpen()) {
return SocketState.UPGRADED;
} else {
return SocketState.CLOSED;
}
}
@Override
public void timeoutAsync(long now) {
// NO-OP
}
@Override
public void pause() {
// NO-OP
}
@Override
public void destroy() {
if (connection != null) {
try {
connection.close();
} catch (Exception e) {
log.error(sm.getString("wsHttpUpgradeHandler.destroyFailed"), e);
}
}
}
private void onError(Throwable throwable) {
// Need to call onError using the web application's class loader
Thread t = Thread.currentThread();
ClassLoader cl = t.getContextClassLoader();
t.setContextClassLoader(applicationClassLoader);
try {
ep.onError(wsSession, throwable);
} finally {
t.setContextClassLoader(cl);
}
}
private void close(CloseReason cr) {
/*
* Any call to this method is a result of a problem reading from the
* client. At this point that state of the connection is unknown.
* Attempt to send a close frame to the client and then close the socket
* immediately. There is no point in waiting for a close frame from the
* client because there is no guarantee that we can recover from
* whatever messed up state the client put the connection into.
*/
wsSession.onClose(cr);
}
@Override
public void setSslSupport(SSLSupport sslSupport) {
// NO-OP. WebSocket has no requirement to access the TLS information
// associated with the underlying connection.
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.server;
import java.util.Map;
import javax.websocket.server.ServerEndpointConfig;
class WsMappingResult {
private final ServerEndpointConfig config;
private final Map<String,String> pathParams;
WsMappingResult(ServerEndpointConfig config,
Map<String,String> pathParams) {
this.config = config;
this.pathParams = pathParams;
}
ServerEndpointConfig getConfig() {
return config;
}
Map<String,String> getPathParams() {
return pathParams;
}
}

View File

@@ -0,0 +1,84 @@
/*
* 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.server;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.websocket.Decoder;
import javax.websocket.Encoder;
import javax.websocket.Extension;
import javax.websocket.server.ServerEndpointConfig;
/**
* Wraps the provided {@link ServerEndpointConfig} and provides a per session
* view - the difference being that the map returned by {@link
* #getUserProperties()} is unique to this instance rather than shared with the
* wrapped {@link ServerEndpointConfig}.
*/
class WsPerSessionServerEndpointConfig implements ServerEndpointConfig {
private final ServerEndpointConfig perEndpointConfig;
private final Map<String,Object> perSessionUserProperties =
new ConcurrentHashMap<>();
WsPerSessionServerEndpointConfig(ServerEndpointConfig perEndpointConfig) {
this.perEndpointConfig = perEndpointConfig;
perSessionUserProperties.putAll(perEndpointConfig.getUserProperties());
}
@Override
public List<Class<? extends Encoder>> getEncoders() {
return perEndpointConfig.getEncoders();
}
@Override
public List<Class<? extends Decoder>> getDecoders() {
return perEndpointConfig.getDecoders();
}
@Override
public Map<String,Object> getUserProperties() {
return perSessionUserProperties;
}
@Override
public Class<?> getEndpointClass() {
return perEndpointConfig.getEndpointClass();
}
@Override
public String getPath() {
return perEndpointConfig.getPath();
}
@Override
public List<String> getSubprotocols() {
return perEndpointConfig.getSubprotocols();
}
@Override
public List<Extension> getExtensions() {
return perEndpointConfig.getExtensions();
}
@Override
public Configurator getConfigurator() {
return perEndpointConfig.getConfigurator();
}
}

View File

@@ -0,0 +1,265 @@
/*
* 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.server;
import java.io.EOFException;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.util.concurrent.Executor;
import javax.websocket.SendHandler;
import javax.websocket.SendResult;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.net.AbstractEndpoint;
import org.apache.tomcat.util.net.SocketWrapperBase;
import org.apache.tomcat.util.res.StringManager;
import org.apache.tomcat.websocket.Transformation;
import org.apache.tomcat.websocket.WsRemoteEndpointImplBase;
/**
* This is the server side {@link javax.websocket.RemoteEndpoint} implementation
* - i.e. what the server uses to send data to the client.
*/
public class WsRemoteEndpointImplServer extends WsRemoteEndpointImplBase {
private static final StringManager sm =
StringManager.getManager(WsRemoteEndpointImplServer.class);
private final Log log = LogFactory.getLog(WsRemoteEndpointImplServer.class); // must not be static
private final SocketWrapperBase<?> socketWrapper;
private final WsWriteTimeout wsWriteTimeout;
private volatile SendHandler handler = null;
private volatile ByteBuffer[] buffers = null;
private volatile long timeoutExpiry = -1;
public WsRemoteEndpointImplServer(SocketWrapperBase<?> socketWrapper,
WsServerContainer serverContainer) {
this.socketWrapper = socketWrapper;
this.wsWriteTimeout = serverContainer.getTimeout();
}
@Override
protected final boolean isMasked() {
return false;
}
@Override
protected void doWrite(SendHandler handler, long blockingWriteTimeoutExpiry,
ByteBuffer... buffers) {
if (blockingWriteTimeoutExpiry == -1) {
this.handler = handler;
this.buffers = buffers;
// This is definitely the same thread that triggered the write so a
// dispatch will be required.
onWritePossible(true);
} else {
// Blocking
try {
for (ByteBuffer buffer : buffers) {
long timeout = blockingWriteTimeoutExpiry - System.currentTimeMillis();
if (timeout <= 0) {
SendResult sr = new SendResult(new SocketTimeoutException());
handler.onResult(sr);
return;
}
socketWrapper.setWriteTimeout(timeout);
socketWrapper.write(true, buffer);
}
long timeout = blockingWriteTimeoutExpiry - System.currentTimeMillis();
if (timeout <= 0) {
SendResult sr = new SendResult(new SocketTimeoutException());
handler.onResult(sr);
return;
}
socketWrapper.setWriteTimeout(timeout);
socketWrapper.flush(true);
handler.onResult(SENDRESULT_OK);
} catch (IOException e) {
SendResult sr = new SendResult(e);
handler.onResult(sr);
}
}
}
public void onWritePossible(boolean useDispatch) {
ByteBuffer[] buffers = this.buffers;
if (buffers == null) {
// Servlet 3.1 will call the write listener once even if nothing
// was written
return;
}
boolean complete = false;
try {
socketWrapper.flush(false);
// If this is false there will be a call back when it is true
while (socketWrapper.isReadyForWrite()) {
complete = true;
for (ByteBuffer buffer : buffers) {
if (buffer.hasRemaining()) {
complete = false;
socketWrapper.write(false, buffer);
break;
}
}
if (complete) {
socketWrapper.flush(false);
complete = socketWrapper.isReadyForWrite();
if (complete) {
wsWriteTimeout.unregister(this);
clearHandler(null, useDispatch);
}
break;
}
}
} catch (IOException | IllegalStateException e) {
wsWriteTimeout.unregister(this);
clearHandler(e, useDispatch);
close();
}
if (!complete) {
// Async write is in progress
long timeout = getSendTimeout();
if (timeout > 0) {
// Register with timeout thread
timeoutExpiry = timeout + System.currentTimeMillis();
wsWriteTimeout.register(this);
}
}
}
@Override
protected void doClose() {
if (handler != null) {
// close() can be triggered by a wide range of scenarios. It is far
// simpler just to always use a dispatch than it is to try and track
// whether or not this method was called by the same thread that
// triggered the write
clearHandler(new EOFException(), true);
}
try {
socketWrapper.close();
} catch (Exception e) {
if (log.isInfoEnabled()) {
log.info(sm.getString("wsRemoteEndpointServer.closeFailed"), e);
}
}
wsWriteTimeout.unregister(this);
}
protected long getTimeoutExpiry() {
return timeoutExpiry;
}
/*
* Currently this is only called from the background thread so we could just
* call clearHandler() with useDispatch == false but the method parameter
* was added in case other callers started to use this method to make sure
* that those callers think through what the correct value of useDispatch is
* for them.
*/
protected void onTimeout(boolean useDispatch) {
if (handler != null) {
clearHandler(new SocketTimeoutException(), useDispatch);
}
close();
}
@Override
protected void setTransformation(Transformation transformation) {
// Overridden purely so it is visible to other classes in this package
super.setTransformation(transformation);
}
/**
*
* @param t The throwable associated with any error that
* occurred
* @param useDispatch Should {@link SendHandler#onResult(SendResult)} be
* called from a new thread, keeping in mind the
* requirements of
* {@link javax.websocket.RemoteEndpoint.Async}
*/
private void clearHandler(Throwable t, boolean useDispatch) {
// Setting the result marks this (partial) message as
// complete which means the next one may be sent which
// could update the value of the handler. Therefore, keep a
// local copy before signalling the end of the (partial)
// message.
SendHandler sh = handler;
handler = null;
buffers = null;
if (sh != null) {
if (useDispatch) {
OnResultRunnable r = new OnResultRunnable(sh, t);
AbstractEndpoint<?> endpoint = socketWrapper.getEndpoint();
Executor containerExecutor = endpoint.getExecutor();
if (endpoint.isRunning() && containerExecutor != null) {
containerExecutor.execute(r);
} else {
// Can't use the executor so call the runnable directly.
// This may not be strictly specification compliant in all
// cases but during shutdown only close messages are going
// to be sent so there should not be the issue of nested
// calls leading to stack overflow as described in bug
// 55715. The issues with nested calls was the reason for
// the separate thread requirement in the specification.
r.run();
}
} else {
if (t == null) {
sh.onResult(new SendResult());
} else {
sh.onResult(new SendResult(t));
}
}
}
}
private static class OnResultRunnable implements Runnable {
private final SendHandler sh;
private final Throwable t;
private OnResultRunnable(SendHandler sh, Throwable t) {
this.sh = sh;
this.t = t;
}
@Override
public void run() {
if (t == null) {
sh.onResult(new SendResult());
} else {
sh.onResult(new SendResult(t));
}
}
}
}

View File

@@ -0,0 +1,151 @@
/*
* 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.server;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.HandlesTypes;
import javax.websocket.ContainerProvider;
import javax.websocket.DeploymentException;
import javax.websocket.Endpoint;
import javax.websocket.server.ServerApplicationConfig;
import javax.websocket.server.ServerEndpoint;
import javax.websocket.server.ServerEndpointConfig;
import org.apache.tomcat.util.compat.JreCompat;
/**
* Registers an interest in any class that is annotated with
* {@link ServerEndpoint} so that Endpoint can be published via the WebSocket
* server.
*/
@HandlesTypes({ServerEndpoint.class, ServerApplicationConfig.class,
Endpoint.class})
public class WsSci implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> clazzes, ServletContext ctx)
throws ServletException {
WsServerContainer sc = init(ctx, true);
if (clazzes == null || clazzes.size() == 0) {
return;
}
// Group the discovered classes by type
Set<ServerApplicationConfig> serverApplicationConfigs = new HashSet<>();
Set<Class<? extends Endpoint>> scannedEndpointClazzes = new HashSet<>();
Set<Class<?>> scannedPojoEndpoints = new HashSet<>();
try {
// wsPackage is "javax.websocket."
String wsPackage = ContainerProvider.class.getName();
wsPackage = wsPackage.substring(0, wsPackage.lastIndexOf('.') + 1);
for (Class<?> clazz : clazzes) {
JreCompat jreCompat = JreCompat.getInstance();
int modifiers = clazz.getModifiers();
if (!Modifier.isPublic(modifiers) ||
Modifier.isAbstract(modifiers) ||
Modifier.isInterface(modifiers) ||
!jreCompat.isExported(clazz)) {
// Non-public, abstract, interface or not in an exported
// package (Java 9+) - skip it.
continue;
}
// Protect against scanning the WebSocket API JARs
if (clazz.getName().startsWith(wsPackage)) {
continue;
}
if (ServerApplicationConfig.class.isAssignableFrom(clazz)) {
serverApplicationConfigs.add(
(ServerApplicationConfig) clazz.getConstructor().newInstance());
}
if (Endpoint.class.isAssignableFrom(clazz)) {
@SuppressWarnings("unchecked")
Class<? extends Endpoint> endpoint =
(Class<? extends Endpoint>) clazz;
scannedEndpointClazzes.add(endpoint);
}
if (clazz.isAnnotationPresent(ServerEndpoint.class)) {
scannedPojoEndpoints.add(clazz);
}
}
} catch (ReflectiveOperationException e) {
throw new ServletException(e);
}
// Filter the results
Set<ServerEndpointConfig> filteredEndpointConfigs = new HashSet<>();
Set<Class<?>> filteredPojoEndpoints = new HashSet<>();
if (serverApplicationConfigs.isEmpty()) {
filteredPojoEndpoints.addAll(scannedPojoEndpoints);
} else {
for (ServerApplicationConfig config : serverApplicationConfigs) {
Set<ServerEndpointConfig> configFilteredEndpoints =
config.getEndpointConfigs(scannedEndpointClazzes);
if (configFilteredEndpoints != null) {
filteredEndpointConfigs.addAll(configFilteredEndpoints);
}
Set<Class<?>> configFilteredPojos =
config.getAnnotatedEndpointClasses(
scannedPojoEndpoints);
if (configFilteredPojos != null) {
filteredPojoEndpoints.addAll(configFilteredPojos);
}
}
}
try {
// Deploy endpoints
for (ServerEndpointConfig config : filteredEndpointConfigs) {
sc.addEndpoint(config);
}
// Deploy POJOs
for (Class<?> clazz : filteredPojoEndpoints) {
sc.addEndpoint(clazz, true);
}
} catch (DeploymentException e) {
throw new ServletException(e);
}
}
static WsServerContainer init(ServletContext servletContext,
boolean initBySciMechanism) {
WsServerContainer sc = new WsServerContainer(servletContext);
servletContext.setAttribute(
Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE, sc);
servletContext.addListener(new WsSessionListener(sc));
// Can't register the ContextListener again if the ContextListener is
// calling this method
if (initBySciMechanism) {
servletContext.addListener(new WsContextListener());
}
return sc;
}
}

View File

@@ -0,0 +1,534 @@
/*
* 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.server;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListMap;
import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.websocket.CloseReason;
import javax.websocket.CloseReason.CloseCodes;
import javax.websocket.DeploymentException;
import javax.websocket.Encoder;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpoint;
import javax.websocket.server.ServerEndpointConfig;
import javax.websocket.server.ServerEndpointConfig.Configurator;
import org.apache.tomcat.InstanceManager;
import org.apache.tomcat.util.res.StringManager;
import org.apache.tomcat.websocket.WsSession;
import org.apache.tomcat.websocket.WsWebSocketContainer;
import org.apache.tomcat.websocket.pojo.PojoMethodMapping;
/**
* Provides a per class loader (i.e. per web application) instance of a
* ServerContainer. Web application wide defaults may be configured by setting
* the following servlet context initialisation parameters to the desired
* values.
* <ul>
* <li>{@link Constants#BINARY_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM}</li>
* <li>{@link Constants#TEXT_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM}</li>
* </ul>
*/
public class WsServerContainer extends WsWebSocketContainer
implements ServerContainer {
private static final StringManager sm = StringManager.getManager(WsServerContainer.class);
private static final CloseReason AUTHENTICATED_HTTP_SESSION_CLOSED =
new CloseReason(CloseCodes.VIOLATED_POLICY,
"This connection was established under an authenticated " +
"HTTP session that has ended.");
private final WsWriteTimeout wsWriteTimeout = new WsWriteTimeout();
private final ServletContext servletContext;
private final Map<String,ExactPathMatch> configExactMatchMap = new ConcurrentHashMap<>();
private final ConcurrentMap<Integer,ConcurrentSkipListMap<String,TemplatePathMatch>> configTemplateMatchMap =
new ConcurrentHashMap<>();
private volatile boolean enforceNoAddAfterHandshake =
org.apache.tomcat.websocket.Constants.STRICT_SPEC_COMPLIANCE;
private volatile boolean addAllowed = true;
private final ConcurrentMap<String,Set<WsSession>> authenticatedSessions =
new ConcurrentHashMap<>();
private volatile boolean endpointsRegistered = false;
private volatile boolean deploymentFailed = false;
WsServerContainer(ServletContext servletContext) {
this.servletContext = servletContext;
setInstanceManager((InstanceManager) servletContext.getAttribute(InstanceManager.class.getName()));
// Configure servlet context wide defaults
String value = servletContext.getInitParameter(
Constants.BINARY_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM);
if (value != null) {
setDefaultMaxBinaryMessageBufferSize(Integer.parseInt(value));
}
value = servletContext.getInitParameter(
Constants.TEXT_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM);
if (value != null) {
setDefaultMaxTextMessageBufferSize(Integer.parseInt(value));
}
value = servletContext.getInitParameter(
Constants.ENFORCE_NO_ADD_AFTER_HANDSHAKE_CONTEXT_INIT_PARAM);
if (value != null) {
setEnforceNoAddAfterHandshake(Boolean.parseBoolean(value));
}
FilterRegistration.Dynamic fr = servletContext.addFilter(
"Tomcat WebSocket (JSR356) Filter", new WsFilter());
fr.setAsyncSupported(true);
EnumSet<DispatcherType> types = EnumSet.of(DispatcherType.REQUEST,
DispatcherType.FORWARD);
fr.addMappingForUrlPatterns(types, true, "/*");
}
/**
* Published the provided endpoint implementation at the specified path with
* the specified configuration. {@link #WsServerContainer(ServletContext)}
* must be called before calling this method.
*
* @param sec The configuration to use when creating endpoint instances
* @throws DeploymentException if the endpoint cannot be published as
* requested
*/
@Override
public void addEndpoint(ServerEndpointConfig sec) throws DeploymentException {
addEndpoint(sec, false);
}
void addEndpoint(ServerEndpointConfig sec, boolean fromAnnotatedPojo) throws DeploymentException {
if (enforceNoAddAfterHandshake && !addAllowed) {
throw new DeploymentException(
sm.getString("serverContainer.addNotAllowed"));
}
if (servletContext == null) {
throw new DeploymentException(
sm.getString("serverContainer.servletContextMissing"));
}
if (deploymentFailed) {
throw new DeploymentException(sm.getString("serverContainer.failedDeployment",
servletContext.getContextPath(), servletContext.getVirtualServerName()));
}
try {
String path = sec.getPath();
// Add method mapping to user properties
PojoMethodMapping methodMapping = new PojoMethodMapping(sec.getEndpointClass(),
sec.getDecoders(), path);
if (methodMapping.getOnClose() != null || methodMapping.getOnOpen() != null
|| methodMapping.getOnError() != null || methodMapping.hasMessageHandlers()) {
sec.getUserProperties().put(org.apache.tomcat.websocket.pojo.Constants.POJO_METHOD_MAPPING_KEY,
methodMapping);
}
UriTemplate uriTemplate = new UriTemplate(path);
if (uriTemplate.hasParameters()) {
Integer key = Integer.valueOf(uriTemplate.getSegmentCount());
ConcurrentSkipListMap<String,TemplatePathMatch> templateMatches =
configTemplateMatchMap.get(key);
if (templateMatches == null) {
// Ensure that if concurrent threads execute this block they
// all end up using the same ConcurrentSkipListMap instance
templateMatches = new ConcurrentSkipListMap<>();
configTemplateMatchMap.putIfAbsent(key, templateMatches);
templateMatches = configTemplateMatchMap.get(key);
}
TemplatePathMatch newMatch = new TemplatePathMatch(sec, uriTemplate, fromAnnotatedPojo);
TemplatePathMatch oldMatch = templateMatches.putIfAbsent(uriTemplate.getNormalizedPath(), newMatch);
if (oldMatch != null) {
// Note: This depends on Endpoint instances being added
// before POJOs in WsSci#onStartup()
if (oldMatch.isFromAnnotatedPojo() && !newMatch.isFromAnnotatedPojo() &&
oldMatch.getConfig().getEndpointClass() == newMatch.getConfig().getEndpointClass()) {
// The WebSocket spec says to ignore the new match in this case
templateMatches.put(path, oldMatch);
} else {
// Duplicate uriTemplate;
throw new DeploymentException(
sm.getString("serverContainer.duplicatePaths", path,
sec.getEndpointClass(),
sec.getEndpointClass()));
}
}
} else {
// Exact match
ExactPathMatch newMatch = new ExactPathMatch(sec, fromAnnotatedPojo);
ExactPathMatch oldMatch = configExactMatchMap.put(path, newMatch);
if (oldMatch != null) {
// Note: This depends on Endpoint instances being added
// before POJOs in WsSci#onStartup()
if (oldMatch.isFromAnnotatedPojo() && !newMatch.isFromAnnotatedPojo() &&
oldMatch.getConfig().getEndpointClass() == newMatch.getConfig().getEndpointClass()) {
// The WebSocket spec says to ignore the new match in this case
configExactMatchMap.put(path, oldMatch);
} else {
// Duplicate path mappings
throw new DeploymentException(
sm.getString("serverContainer.duplicatePaths", path,
oldMatch.getConfig().getEndpointClass(),
sec.getEndpointClass()));
}
}
}
endpointsRegistered = true;
} catch (DeploymentException de) {
failDeployment();
throw de;
}
}
/**
* Provides the equivalent of {@link #addEndpoint(ServerEndpointConfig)}
* for publishing plain old java objects (POJOs) that have been annotated as
* WebSocket endpoints.
*
* @param pojo The annotated POJO
*/
@Override
public void addEndpoint(Class<?> pojo) throws DeploymentException {
addEndpoint(pojo, false);
}
void addEndpoint(Class<?> pojo, boolean fromAnnotatedPojo) throws DeploymentException {
if (deploymentFailed) {
throw new DeploymentException(sm.getString("serverContainer.failedDeployment",
servletContext.getContextPath(), servletContext.getVirtualServerName()));
}
ServerEndpointConfig sec;
try {
ServerEndpoint annotation = pojo.getAnnotation(ServerEndpoint.class);
if (annotation == null) {
throw new DeploymentException(
sm.getString("serverContainer.missingAnnotation",
pojo.getName()));
}
String path = annotation.value();
// Validate encoders
validateEncoders(annotation.encoders());
// ServerEndpointConfig
Class<? extends Configurator> configuratorClazz =
annotation.configurator();
Configurator configurator = null;
if (!configuratorClazz.equals(Configurator.class)) {
try {
configurator = annotation.configurator().getConstructor().newInstance();
} catch (ReflectiveOperationException e) {
throw new DeploymentException(sm.getString(
"serverContainer.configuratorFail",
annotation.configurator().getName(),
pojo.getClass().getName()), e);
}
}
sec = ServerEndpointConfig.Builder.create(pojo, path).
decoders(Arrays.asList(annotation.decoders())).
encoders(Arrays.asList(annotation.encoders())).
subprotocols(Arrays.asList(annotation.subprotocols())).
configurator(configurator).
build();
} catch (DeploymentException de) {
failDeployment();
throw de;
}
addEndpoint(sec, fromAnnotatedPojo);
}
void failDeployment() {
deploymentFailed = true;
// Clear all existing deployments
endpointsRegistered = false;
configExactMatchMap.clear();
configTemplateMatchMap.clear();
}
boolean areEndpointsRegistered() {
return endpointsRegistered;
}
/**
* Until the WebSocket specification provides such a mechanism, this Tomcat
* proprietary method is provided to enable applications to programmatically
* determine whether or not to upgrade an individual request to WebSocket.
* <p>
* Note: This method is not used by Tomcat but is used directly by
* third-party code and must not be removed.
*
* @param request The request object to be upgraded
* @param response The response object to be populated with the result of
* the upgrade
* @param sec The server endpoint to use to process the upgrade request
* @param pathParams The path parameters associated with the upgrade request
*
* @throws ServletException If a configuration error prevents the upgrade
* from taking place
* @throws IOException If an I/O error occurs during the upgrade process
*/
public void doUpgrade(HttpServletRequest request,
HttpServletResponse response, ServerEndpointConfig sec,
Map<String,String> pathParams)
throws ServletException, IOException {
UpgradeUtil.doUpgrade(this, request, response, sec, pathParams);
}
public WsMappingResult findMapping(String path) {
// Prevent registering additional endpoints once the first attempt has
// been made to use one
if (addAllowed) {
addAllowed = false;
}
// Check an exact match. Simple case as there are no templates.
ExactPathMatch match = configExactMatchMap.get(path);
if (match != null) {
return new WsMappingResult(match.getConfig(), Collections.<String, String>emptyMap());
}
// No exact match. Need to look for template matches.
UriTemplate pathUriTemplate = null;
try {
pathUriTemplate = new UriTemplate(path);
} catch (DeploymentException e) {
// Path is not valid so can't be matched to a WebSocketEndpoint
return null;
}
// Number of segments has to match
Integer key = Integer.valueOf(pathUriTemplate.getSegmentCount());
ConcurrentSkipListMap<String,TemplatePathMatch> templateMatches = configTemplateMatchMap.get(key);
if (templateMatches == null) {
// No templates with an equal number of segments so there will be
// no matches
return null;
}
// List is in alphabetical order of normalised templates.
// Correct match is the first one that matches.
ServerEndpointConfig sec = null;
Map<String,String> pathParams = null;
for (TemplatePathMatch templateMatch : templateMatches.values()) {
pathParams = templateMatch.getUriTemplate().match(pathUriTemplate);
if (pathParams != null) {
sec = templateMatch.getConfig();
break;
}
}
if (sec == null) {
// No match
return null;
}
return new WsMappingResult(sec, pathParams);
}
public boolean isEnforceNoAddAfterHandshake() {
return enforceNoAddAfterHandshake;
}
public void setEnforceNoAddAfterHandshake(
boolean enforceNoAddAfterHandshake) {
this.enforceNoAddAfterHandshake = enforceNoAddAfterHandshake;
}
protected WsWriteTimeout getTimeout() {
return wsWriteTimeout;
}
/**
* {@inheritDoc}
*
* Overridden to make it visible to other classes in this package.
*/
@Override
protected void registerSession(Object key, WsSession wsSession) {
super.registerSession(key, wsSession);
if (wsSession.isOpen() &&
wsSession.getUserPrincipal() != null &&
wsSession.getHttpSessionId() != null) {
registerAuthenticatedSession(wsSession,
wsSession.getHttpSessionId());
}
}
/**
* {@inheritDoc}
*
* Overridden to make it visible to other classes in this package.
*/
@Override
protected void unregisterSession(Object key, WsSession wsSession) {
if (wsSession.getUserPrincipal() != null &&
wsSession.getHttpSessionId() != null) {
unregisterAuthenticatedSession(wsSession,
wsSession.getHttpSessionId());
}
super.unregisterSession(key, wsSession);
}
private void registerAuthenticatedSession(WsSession wsSession,
String httpSessionId) {
Set<WsSession> wsSessions = authenticatedSessions.get(httpSessionId);
if (wsSessions == null) {
wsSessions = Collections.newSetFromMap(
new ConcurrentHashMap<WsSession,Boolean>());
authenticatedSessions.putIfAbsent(httpSessionId, wsSessions);
wsSessions = authenticatedSessions.get(httpSessionId);
}
wsSessions.add(wsSession);
}
private void unregisterAuthenticatedSession(WsSession wsSession,
String httpSessionId) {
Set<WsSession> wsSessions = authenticatedSessions.get(httpSessionId);
// wsSessions will be null if the HTTP session has ended
if (wsSessions != null) {
wsSessions.remove(wsSession);
}
}
public void closeAuthenticatedSession(String httpSessionId) {
Set<WsSession> wsSessions = authenticatedSessions.remove(httpSessionId);
if (wsSessions != null && !wsSessions.isEmpty()) {
for (WsSession wsSession : wsSessions) {
try {
wsSession.close(AUTHENTICATED_HTTP_SESSION_CLOSED);
} catch (IOException e) {
// Any IOExceptions during close will have been caught and the
// onError method called.
}
}
}
}
private static void validateEncoders(Class<? extends Encoder>[] encoders)
throws DeploymentException {
for (Class<? extends Encoder> encoder : encoders) {
// Need to instantiate decoder to ensure it is valid and that
// deployment can be failed if it is not
@SuppressWarnings("unused")
Encoder instance;
try {
encoder.getConstructor().newInstance();
} catch(ReflectiveOperationException e) {
throw new DeploymentException(sm.getString(
"serverContainer.encoderFail", encoder.getName()), e);
}
}
}
private static class TemplatePathMatch {
private final ServerEndpointConfig config;
private final UriTemplate uriTemplate;
private final boolean fromAnnotatedPojo;
public TemplatePathMatch(ServerEndpointConfig config, UriTemplate uriTemplate,
boolean fromAnnotatedPojo) {
this.config = config;
this.uriTemplate = uriTemplate;
this.fromAnnotatedPojo = fromAnnotatedPojo;
}
public ServerEndpointConfig getConfig() {
return config;
}
public UriTemplate getUriTemplate() {
return uriTemplate;
}
public boolean isFromAnnotatedPojo() {
return fromAnnotatedPojo;
}
}
private static class ExactPathMatch {
private final ServerEndpointConfig config;
private final boolean fromAnnotatedPojo;
public ExactPathMatch(ServerEndpointConfig config, boolean fromAnnotatedPojo) {
this.config = config;
this.fromAnnotatedPojo = fromAnnotatedPojo;
}
public ServerEndpointConfig getConfig() {
return config;
}
public boolean isFromAnnotatedPojo() {
return fromAnnotatedPojo;
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.server;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
public class WsSessionListener implements HttpSessionListener{
private final WsServerContainer wsServerContainer;
public WsSessionListener(WsServerContainer wsServerContainer) {
this.wsServerContainer = wsServerContainer;
}
@Override
public void sessionCreated(HttpSessionEvent se) {
// NO-OP
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
wsServerContainer.closeAuthenticatedSession(se.getSession().getId());
}
}

View File

@@ -0,0 +1,128 @@
/*
* 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.server;
import java.util.Comparator;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.tomcat.websocket.BackgroundProcess;
import org.apache.tomcat.websocket.BackgroundProcessManager;
/**
* Provides timeouts for asynchronous web socket writes. On the server side we
* only have access to {@link javax.servlet.ServletOutputStream} and
* {@link javax.servlet.ServletInputStream} so there is no way to set a timeout
* for writes to the client.
*/
public class WsWriteTimeout implements BackgroundProcess {
private final Set<WsRemoteEndpointImplServer> endpoints =
new ConcurrentSkipListSet<>(new EndpointComparator());
private final AtomicInteger count = new AtomicInteger(0);
private int backgroundProcessCount = 0;
private volatile int processPeriod = 1;
@Override
public void backgroundProcess() {
// This method gets called once a second.
backgroundProcessCount ++;
if (backgroundProcessCount >= processPeriod) {
backgroundProcessCount = 0;
long now = System.currentTimeMillis();
for (WsRemoteEndpointImplServer endpoint : endpoints) {
if (endpoint.getTimeoutExpiry() < now) {
// Background thread, not the thread that triggered the
// write so no need to use a dispatch
endpoint.onTimeout(false);
} else {
// Endpoints are ordered by timeout expiry so if this point
// is reached there is no need to check the remaining
// endpoints
break;
}
}
}
}
@Override
public void setProcessPeriod(int period) {
this.processPeriod = period;
}
/**
* {@inheritDoc}
*
* The default value is 1 which means asynchronous write timeouts are
* processed every 1 second.
*/
@Override
public int getProcessPeriod() {
return processPeriod;
}
public void register(WsRemoteEndpointImplServer endpoint) {
boolean result = endpoints.add(endpoint);
if (result) {
int newCount = count.incrementAndGet();
if (newCount == 1) {
BackgroundProcessManager.getInstance().register(this);
}
}
}
public void unregister(WsRemoteEndpointImplServer endpoint) {
boolean result = endpoints.remove(endpoint);
if (result) {
int newCount = count.decrementAndGet();
if (newCount == 0) {
BackgroundProcessManager.getInstance().unregister(this);
}
}
}
/**
* Note: this comparator imposes orderings that are inconsistent with equals
*/
private static class EndpointComparator implements
Comparator<WsRemoteEndpointImplServer> {
@Override
public int compare(WsRemoteEndpointImplServer o1,
WsRemoteEndpointImplServer o2) {
long t1 = o1.getTimeoutExpiry();
long t2 = o2.getTimeoutExpiry();
if (t1 < t2) {
return -1;
} else if (t1 == t2) {
return 0;
} else {
return 1;
}
}
}
}

View File

@@ -0,0 +1,21 @@
/*
* 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.
*/
/**
* Server-side specific implementation classes. These are in a separate package
* to make packaging a pure client JAR simpler.
*/
package org.apache.tomcat.websocket.server;