summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKonstantin Pavlov <thresh@nginx.com>2019-09-19 19:04:16 +0300
committerKonstantin Pavlov <thresh@nginx.com>2019-09-19 19:04:16 +0300
commitdeb26fa47a9ab1b358938134a8ced8bbc4a083e1 (patch)
tree0bedf8829f003fa4c0101e3421b7184acc1c8343
parentfcb1f851d0b5d1774a6cb876288ea29cfef58618 (diff)
parentdb777d1e7f607d1b0f01dfb73ad0bac12987202b (diff)
downloadunit-deb26fa47a9ab1b358938134a8ced8bbc4a083e1.tar.gz
unit-deb26fa47a9ab1b358938134a8ced8bbc4a083e1.tar.bz2
Merged with the default branch.
-rw-r--r--.hgtags1
-rw-r--r--CHANGES19
-rw-r--r--auto/capability19
-rw-r--r--auto/isolation52
-rw-r--r--auto/modules/java166
-rw-r--r--auto/sources8
-rw-r--r--auto/summary2
-rwxr-xr-xconfigure2
-rw-r--r--docs/changes.xml65
-rw-r--r--pkg/docker/Dockerfile.full2
-rw-r--r--pkg/docker/Dockerfile.go1.7-dev2
-rw-r--r--pkg/docker/Dockerfile.go1.8-dev2
-rw-r--r--pkg/docker/Dockerfile.minimal2
-rw-r--r--pkg/docker/Dockerfile.perl5.242
-rw-r--r--pkg/docker/Dockerfile.php7.02
-rw-r--r--pkg/docker/Dockerfile.python2.72
-rw-r--r--pkg/docker/Dockerfile.python3.52
-rw-r--r--pkg/docker/Dockerfile.ruby2.32
-rw-r--r--pkg/rpm/Makefile.jsc-common4
-rw-r--r--pkg/rpm/Makefile.jsc113
-rw-r--r--pkg/rpm/Makefile.jsc85
-rw-r--r--src/go/unit/nxt_cgo_lib.c5
-rw-r--r--src/go/unit/request.go2
-rw-r--r--src/go/unit/unit.go49
-rw-r--r--src/java/javax/websocket/ClientEndpoint.java34
-rw-r--r--src/java/javax/websocket/ClientEndpointConfig.java138
-rw-r--r--src/java/javax/websocket/CloseReason.java122
-rw-r--r--src/java/javax/websocket/ContainerProvider.java63
-rw-r--r--src/java/javax/websocket/DecodeException.java56
-rw-r--r--src/java/javax/websocket/Decoder.java53
-rw-r--r--src/java/javax/websocket/DefaultClientEndpointConfig.java80
-rw-r--r--src/java/javax/websocket/DeploymentException.java30
-rw-r--r--src/java/javax/websocket/EncodeException.java38
-rw-r--r--src/java/javax/websocket/Encoder.java51
-rw-r--r--src/java/javax/websocket/Endpoint.java49
-rw-r--r--src/java/javax/websocket/EndpointConfig.java29
-rw-r--r--src/java/javax/websocket/Extension.java29
-rw-r--r--src/java/javax/websocket/HandshakeResponse.java30
-rw-r--r--src/java/javax/websocket/MessageHandler.java42
-rw-r--r--src/java/javax/websocket/OnClose.java27
-rw-r--r--src/java/javax/websocket/OnError.java27
-rw-r--r--src/java/javax/websocket/OnMessage.java28
-rw-r--r--src/java/javax/websocket/OnOpen.java27
-rw-r--r--src/java/javax/websocket/PongMessage.java32
-rw-r--r--src/java/javax/websocket/RemoteEndpoint.java229
-rw-r--r--src/java/javax/websocket/SendHandler.java22
-rw-r--r--src/java/javax/websocket/SendResult.java39
-rw-r--r--src/java/javax/websocket/Session.java193
-rw-r--r--src/java/javax/websocket/SessionException.java35
-rw-r--r--src/java/javax/websocket/WebSocketContainer.java131
-rw-r--r--src/java/javax/websocket/server/DefaultServerEndpointConfig.java95
-rw-r--r--src/java/javax/websocket/server/HandshakeRequest.java53
-rw-r--r--src/java/javax/websocket/server/PathParam.java33
-rw-r--r--src/java/javax/websocket/server/ServerApplicationConfig.java51
-rw-r--r--src/java/javax/websocket/server/ServerContainer.java30
-rw-r--r--src/java/javax/websocket/server/ServerEndpoint.java46
-rw-r--r--src/java/javax/websocket/server/ServerEndpointConfig.java218
-rw-r--r--src/java/nginx/unit/Context.java139
-rw-r--r--src/java/nginx/unit/Request.java90
-rw-r--r--src/java/nginx/unit/websocket/AsyncChannelGroupUtil.java151
-rw-r--r--src/java/nginx/unit/websocket/AsyncChannelWrapper.java47
-rw-r--r--src/java/nginx/unit/websocket/AsyncChannelWrapperNonSecure.java112
-rw-r--r--src/java/nginx/unit/websocket/AsyncChannelWrapperSecure.java578
-rw-r--r--src/java/nginx/unit/websocket/AuthenticationException.java35
-rw-r--r--src/java/nginx/unit/websocket/Authenticator.java71
-rw-r--r--src/java/nginx/unit/websocket/AuthenticatorFactory.java68
-rw-r--r--src/java/nginx/unit/websocket/BackgroundProcess.java26
-rw-r--r--src/java/nginx/unit/websocket/BackgroundProcessManager.java149
-rw-r--r--src/java/nginx/unit/websocket/BasicAuthenticator.java66
-rw-r--r--src/java/nginx/unit/websocket/Constants.java158
-rw-r--r--src/java/nginx/unit/websocket/DecoderEntry.java39
-rw-r--r--src/java/nginx/unit/websocket/DigestAuthenticator.java150
-rw-r--r--src/java/nginx/unit/websocket/FutureToSendHandler.java112
-rw-r--r--src/java/nginx/unit/websocket/LocalStrings.properties147
-rw-r--r--src/java/nginx/unit/websocket/MessageHandlerResult.java42
-rw-r--r--src/java/nginx/unit/websocket/MessageHandlerResultType.java23
-rw-r--r--src/java/nginx/unit/websocket/MessagePart.java83
-rw-r--r--src/java/nginx/unit/websocket/PerMessageDeflate.java476
-rw-r--r--src/java/nginx/unit/websocket/ReadBufferOverflowException.java34
-rw-r--r--src/java/nginx/unit/websocket/Transformation.java111
-rw-r--r--src/java/nginx/unit/websocket/TransformationFactory.java51
-rw-r--r--src/java/nginx/unit/websocket/TransformationResult.java37
-rw-r--r--src/java/nginx/unit/websocket/Util.java666
-rw-r--r--src/java/nginx/unit/websocket/WrappedMessageHandler.java25
-rw-r--r--src/java/nginx/unit/websocket/WsContainerProvider.java28
-rw-r--r--src/java/nginx/unit/websocket/WsExtension.java46
-rw-r--r--src/java/nginx/unit/websocket/WsExtensionParameter.java40
-rw-r--r--src/java/nginx/unit/websocket/WsFrameBase.java1010
-rw-r--r--src/java/nginx/unit/websocket/WsFrameClient.java228
-rw-r--r--src/java/nginx/unit/websocket/WsHandshakeResponse.java56
-rw-r--r--src/java/nginx/unit/websocket/WsIOException.java41
-rw-r--r--src/java/nginx/unit/websocket/WsPongMessage.java39
-rw-r--r--src/java/nginx/unit/websocket/WsRemoteEndpointAsync.java79
-rw-r--r--src/java/nginx/unit/websocket/WsRemoteEndpointBase.java64
-rw-r--r--src/java/nginx/unit/websocket/WsRemoteEndpointBasic.java76
-rw-r--r--src/java/nginx/unit/websocket/WsRemoteEndpointImplBase.java1234
-rw-r--r--src/java/nginx/unit/websocket/WsRemoteEndpointImplClient.java75
-rw-r--r--src/java/nginx/unit/websocket/WsSession.java1070
-rw-r--r--src/java/nginx/unit/websocket/WsWebSocketContainer.java1123
-rw-r--r--src/java/nginx/unit/websocket/pojo/Constants.java32
-rw-r--r--src/java/nginx/unit/websocket/pojo/LocalStrings.properties40
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoEndpointBase.java156
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoEndpointClient.java47
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoEndpointServer.java66
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMessageHandlerBase.java122
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBase.java77
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBinary.java36
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialText.java35
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBase.java94
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBinary.java131
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholePong.java48
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeText.java136
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMethodMapping.java731
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoPathParam.java47
-rw-r--r--src/java/nginx/unit/websocket/pojo/package-info.java21
-rw-r--r--src/java/nginx/unit/websocket/server/Constants.java38
-rw-r--r--src/java/nginx/unit/websocket/server/DefaultServerEndpointConfigurator.java88
-rw-r--r--src/java/nginx/unit/websocket/server/LocalStrings.properties43
-rw-r--r--src/java/nginx/unit/websocket/server/UpgradeUtil.java285
-rw-r--r--src/java/nginx/unit/websocket/server/UriTemplate.java177
-rw-r--r--src/java/nginx/unit/websocket/server/WsContextListener.java51
-rw-r--r--src/java/nginx/unit/websocket/server/WsFilter.java81
-rw-r--r--src/java/nginx/unit/websocket/server/WsHandshakeRequest.java196
-rw-r--r--src/java/nginx/unit/websocket/server/WsHttpUpgradeHandler.java172
-rw-r--r--src/java/nginx/unit/websocket/server/WsMappingResult.java44
-rw-r--r--src/java/nginx/unit/websocket/server/WsPerSessionServerEndpointConfig.java84
-rw-r--r--src/java/nginx/unit/websocket/server/WsRemoteEndpointImplServer.java158
-rw-r--r--src/java/nginx/unit/websocket/server/WsSci.java145
-rw-r--r--src/java/nginx/unit/websocket/server/WsServerContainer.java470
-rw-r--r--src/java/nginx/unit/websocket/server/WsSessionListener.java36
-rw-r--r--src/java/nginx/unit/websocket/server/WsWriteTimeout.java128
-rw-r--r--src/java/nginx/unit/websocket/server/package-info.java21
-rw-r--r--src/java/nxt_jni_Request.c144
-rw-r--r--src/java/nxt_jni_Request.h5
-rw-r--r--src/nxt_application.h2
-rw-r--r--src/nxt_capability.c104
-rw-r--r--src/nxt_capability.h17
-rw-r--r--src/nxt_clone.c263
-rw-r--r--src/nxt_clone.h17
-rw-r--r--src/nxt_conf.c73
-rw-r--r--src/nxt_conf.h1
-rw-r--r--src/nxt_conf_validation.c432
-rw-r--r--src/nxt_controller.c2
-rw-r--r--src/nxt_h1proto.c25
-rw-r--r--src/nxt_h1proto_websocket.c8
-rw-r--r--src/nxt_http.h35
-rw-r--r--src/nxt_http_error.c6
-rw-r--r--src/nxt_http_parse.c125
-rw-r--r--src/nxt_http_parse.h17
-rw-r--r--src/nxt_http_request.c30
-rw-r--r--src/nxt_http_route.c28
-rw-r--r--src/nxt_http_static.c599
-rw-r--r--src/nxt_java.c72
-rw-r--r--src/nxt_main.h1
-rw-r--r--src/nxt_main_process.c216
-rw-r--r--src/nxt_port.c2
-rw-r--r--src/nxt_process.c251
-rw-r--r--src/nxt_process.h31
-rw-r--r--src/nxt_router.c102
-rw-r--r--src/nxt_router.h6
-rw-r--r--src/nxt_runtime.c14
-rw-r--r--src/nxt_runtime.h1
-rw-r--r--src/nxt_sprintf.c4
-rw-r--r--src/nxt_string.c122
-rw-r--r--src/nxt_string.h3
-rw-r--r--src/nxt_unit.c96
-rw-r--r--src/test/nxt_http_parse_test.c69
-rw-r--r--test/go/ns_inspect/app.go79
-rw-r--r--test/java/websockets_mirror/app.java57
-rw-r--r--test/test_access_log.py2
-rw-r--r--test/test_configuration.py2
-rw-r--r--test/test_go_application.py2
-rw-r--r--test/test_go_isolation.py135
-rw-r--r--test/test_http_header.py2
-rw-r--r--test/test_java_application.py2
-rw-r--r--test/test_java_websockets.py1469
-rw-r--r--test/test_node_application.py2
-rw-r--r--test/test_node_websockets.py529
-rw-r--r--test/test_perl_application.py2
-rw-r--r--test/test_php_application.py2
-rw-r--r--test/test_php_basic.py2
-rw-r--r--test/test_python_application.py33
-rw-r--r--test/test_python_basic.py2
-rw-r--r--test/test_python_environment.py2
-rw-r--r--test/test_python_procman.py2
-rw-r--r--test/test_routing.py2
-rw-r--r--test/test_routing_tls.py2
-rw-r--r--test/test_ruby_application.py2
-rw-r--r--test/test_settings.py2
-rw-r--r--test/test_static.py376
-rw-r--r--test/test_tls.py2
-rw-r--r--test/unit/applications/lang/go.py32
-rw-r--r--test/unit/applications/lang/java.py26
-rw-r--r--test/unit/applications/lang/node.py12
-rw-r--r--test/unit/applications/tls.py22
-rw-r--r--test/unit/applications/websockets.py46
-rw-r--r--test/unit/feature/isolation.py87
-rw-r--r--test/unit/http.py12
-rw-r--r--test/unit/main.py178
-rw-r--r--version4
200 files changed, 20643 insertions, 922 deletions
diff --git a/.hgtags b/.hgtags
index 7dea0ed0..67e7f47e 100644
--- a/.hgtags
+++ b/.hgtags
@@ -26,3 +26,4 @@ c927a739754ade5ecf7be8da30f6c42446e72d8c 1.9.0-1
cdbba3c3e3762eacc308a5407877c3665a05058d 1.10.0
01322f5ec4e37fd0fd8d9d3e491e8539341160f3 1.10.0-1
b651ff72ffe080835f884a1ace8fa24eb33e3569 1.10.0-2
+3b1601ac0f2f53fed4cae01b9db0e4e070665cae 1.11.0
diff --git a/CHANGES b/CHANGES
index 1c2852dc..76f07a38 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,4 +1,23 @@
+Changes with Unit 1.11.0 19 Sep 2019
+
+ *) Feature: basic support for serving static files.
+
+ *) Feature: isolation of application processes with Linux namespaces.
+
+ *) Feature: built-in WebSocket server implementation for Java Servlet
+ Containers.
+
+ *) Feature: direct addressing of API configuration options containing
+ slashes "/" using URI encoding (%2F).
+
+ *) Bugfix: segmentation fault might have occurred in Go applications
+ under high load.
+
+ *) Bugfix: WebSocket support was broken if Unit was built with some
+ linkers other than GNU ld (e.g. gold or LLD).
+
+
Changes with Unit 1.10.0 22 Aug 2019
*) Change: matching of cookies in routes made case sensitive.
diff --git a/auto/capability b/auto/capability
new file mode 100644
index 00000000..48777665
--- /dev/null
+++ b/auto/capability
@@ -0,0 +1,19 @@
+
+# Copyright (C) Igor Sysoev
+# Copyright (C) NGINX, Inc.
+
+# Linux capability
+
+nxt_feature="Linux capability"
+nxt_feature_name=NXT_HAVE_LINUX_CAPABILITY
+nxt_feature_test="#include <linux/capability.h>
+ #include <unistd.h>
+ #include <sys/syscall.h>
+
+ int main() {
+ struct __user_cap_header_struct hdr;
+ hdr.version = _LINUX_CAPABILITY_VERSION;
+ syscall(SYS_capget, &hdr, 0);
+ return 0;
+ }"
+. auto/feature
diff --git a/auto/isolation b/auto/isolation
new file mode 100644
index 00000000..c26a4991
--- /dev/null
+++ b/auto/isolation
@@ -0,0 +1,52 @@
+# Copyright (C) Igor Sysoev
+# Copyright (C) NGINX, Inc.
+
+# Linux clone syscall.
+
+NXT_ISOLATION=NO
+NXT_HAVE_CLONE=NO
+
+nsflags="USER NS PID NET UTS CGROUP"
+
+nxt_feature="clone(2)"
+nxt_feature_name=NXT_HAVE_CLONE
+nxt_feature_run=no
+nxt_feature_incs=
+nxt_feature_libs=
+nxt_feature_test="#include <sys/wait.h>
+ #include <sys/syscall.h>
+
+ int main() {
+ return __NR_clone | SIGCHLD;
+ }"
+. auto/feature
+
+if [ $nxt_found = yes ]; then
+ NXT_HAVE_CLONE=YES
+
+ # Test all isolation flags
+ for flag in $nsflags; do
+ nxt_feature="CLONE_NEW${flag}"
+ nxt_feature_name=NXT_HAVE_CLONE_NEW${flag}
+ nxt_feature_run=no
+ nxt_feature_incs=
+ nxt_feature_libs=
+ nxt_feature_test="#define _GNU_SOURCE
+ #include <sys/wait.h>
+ #include <sys/syscall.h>
+ #include <sched.h>
+
+ int main() {
+ return CLONE_NEW$flag;
+ }"
+ . auto/feature
+
+ if [ $nxt_found = yes ]; then
+ if [ "$NXT_ISOLATION" = "NO" ]; then
+ NXT_ISOLATION=$flag
+ else
+ NXT_ISOLATION="$NXT_ISOLATION $flag"
+ fi
+ fi
+ done
+fi
diff --git a/auto/modules/java b/auto/modules/java
index 27030da1..24ee4927 100644
--- a/auto/modules/java
+++ b/auto/modules/java
@@ -214,6 +214,7 @@ fi
. ./version
NXT_UNIT_JAR=nginx-unit-jsc-${NXT_JAVA_MODULE}-$NXT_VERSION.jar
+NXT_WS_API_JAR=websocket-api-${NXT_JAVA_MODULE}-$NXT_VERSION.jar
NXT_JAVA_BUILD_CP=$NXT_BUILD_DIR/$NXT_JAVA_MODULE
NXT_JAVA_INSTALL_JARS=
@@ -269,6 +270,7 @@ NXT_JAR_NAMESPACE=org/eclipse/jdt/
. auto/modules/java_get_jar
cat << END >> $NXT_JAVA_JARS
+ "$NXT_WS_API_JAR",
NULL
};
@@ -367,6 +369,112 @@ NXT_JAVA_SRCS=" \
src/java/nginx/unit/SessionAttrProxy.java \
src/java/nginx/unit/Taglib.java \
src/java/nginx/unit/UnitSessionCookieConfig.java \
+ src/java/nginx/unit/websocket/AsyncChannelGroupUtil.java \
+ src/java/nginx/unit/websocket/AsyncChannelWrapper.java \
+ src/java/nginx/unit/websocket/AsyncChannelWrapperNonSecure.java \
+ src/java/nginx/unit/websocket/AsyncChannelWrapperSecure.java \
+ src/java/nginx/unit/websocket/AuthenticationException.java \
+ src/java/nginx/unit/websocket/Authenticator.java \
+ src/java/nginx/unit/websocket/AuthenticatorFactory.java \
+ src/java/nginx/unit/websocket/BackgroundProcess.java \
+ src/java/nginx/unit/websocket/BackgroundProcessManager.java \
+ src/java/nginx/unit/websocket/BasicAuthenticator.java \
+ src/java/nginx/unit/websocket/Constants.java \
+ src/java/nginx/unit/websocket/DecoderEntry.java \
+ src/java/nginx/unit/websocket/DigestAuthenticator.java \
+ src/java/nginx/unit/websocket/FutureToSendHandler.java \
+ src/java/nginx/unit/websocket/MessageHandlerResult.java \
+ src/java/nginx/unit/websocket/MessageHandlerResultType.java \
+ src/java/nginx/unit/websocket/MessagePart.java \
+ src/java/nginx/unit/websocket/PerMessageDeflate.java \
+ src/java/nginx/unit/websocket/ReadBufferOverflowException.java \
+ src/java/nginx/unit/websocket/Transformation.java \
+ src/java/nginx/unit/websocket/TransformationFactory.java \
+ src/java/nginx/unit/websocket/TransformationResult.java \
+ src/java/nginx/unit/websocket/Util.java \
+ src/java/nginx/unit/websocket/WrappedMessageHandler.java \
+ src/java/nginx/unit/websocket/WsContainerProvider.java \
+ src/java/nginx/unit/websocket/WsExtension.java \
+ src/java/nginx/unit/websocket/WsExtensionParameter.java \
+ src/java/nginx/unit/websocket/WsFrameBase.java \
+ src/java/nginx/unit/websocket/WsFrameClient.java \
+ src/java/nginx/unit/websocket/WsHandshakeResponse.java \
+ src/java/nginx/unit/websocket/WsIOException.java \
+ src/java/nginx/unit/websocket/WsPongMessage.java \
+ src/java/nginx/unit/websocket/WsRemoteEndpointAsync.java \
+ src/java/nginx/unit/websocket/WsRemoteEndpointBase.java \
+ src/java/nginx/unit/websocket/WsRemoteEndpointBasic.java \
+ src/java/nginx/unit/websocket/WsRemoteEndpointImplBase.java \
+ src/java/nginx/unit/websocket/WsRemoteEndpointImplClient.java \
+ src/java/nginx/unit/websocket/WsSession.java \
+ src/java/nginx/unit/websocket/WsWebSocketContainer.java \
+ src/java/nginx/unit/websocket/pojo/Constants.java \
+ src/java/nginx/unit/websocket/pojo/PojoEndpointBase.java \
+ src/java/nginx/unit/websocket/pojo/PojoEndpointClient.java \
+ src/java/nginx/unit/websocket/pojo/PojoEndpointServer.java \
+ src/java/nginx/unit/websocket/pojo/PojoMessageHandlerBase.java \
+ src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBase.java \
+ src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBinary.java \
+ src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialText.java \
+ src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBase.java \
+ src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBinary.java \
+ src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholePong.java \
+ src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeText.java \
+ src/java/nginx/unit/websocket/pojo/PojoMethodMapping.java \
+ src/java/nginx/unit/websocket/pojo/PojoPathParam.java \
+ src/java/nginx/unit/websocket/pojo/package-info.java \
+ src/java/nginx/unit/websocket/server/Constants.java \
+ src/java/nginx/unit/websocket/server/DefaultServerEndpointConfigurator.java \
+ src/java/nginx/unit/websocket/server/UpgradeUtil.java \
+ src/java/nginx/unit/websocket/server/UriTemplate.java \
+ src/java/nginx/unit/websocket/server/WsContextListener.java \
+ src/java/nginx/unit/websocket/server/WsFilter.java \
+ src/java/nginx/unit/websocket/server/WsHandshakeRequest.java \
+ src/java/nginx/unit/websocket/server/WsHttpUpgradeHandler.java \
+ src/java/nginx/unit/websocket/server/WsMappingResult.java \
+ src/java/nginx/unit/websocket/server/WsPerSessionServerEndpointConfig.java \
+ src/java/nginx/unit/websocket/server/WsRemoteEndpointImplServer.java \
+ src/java/nginx/unit/websocket/server/WsSci.java \
+ src/java/nginx/unit/websocket/server/WsServerContainer.java \
+ src/java/nginx/unit/websocket/server/WsSessionListener.java \
+ src/java/nginx/unit/websocket/server/WsWriteTimeout.java \
+ src/java/nginx/unit/websocket/server/package-info.java \
+"
+
+NXT_JAVA_WS_API_SRCS=" \
+ src/java/javax/websocket/ClientEndpoint.java \
+ src/java/javax/websocket/ClientEndpointConfig.java \
+ src/java/javax/websocket/CloseReason.java \
+ src/java/javax/websocket/ContainerProvider.java \
+ src/java/javax/websocket/DecodeException.java \
+ src/java/javax/websocket/Decoder.java \
+ src/java/javax/websocket/DefaultClientEndpointConfig.java \
+ src/java/javax/websocket/DeploymentException.java \
+ src/java/javax/websocket/EncodeException.java \
+ src/java/javax/websocket/Encoder.java \
+ src/java/javax/websocket/Endpoint.java \
+ src/java/javax/websocket/EndpointConfig.java \
+ src/java/javax/websocket/Extension.java \
+ src/java/javax/websocket/HandshakeResponse.java \
+ src/java/javax/websocket/MessageHandler.java \
+ src/java/javax/websocket/OnClose.java \
+ src/java/javax/websocket/OnError.java \
+ src/java/javax/websocket/OnMessage.java \
+ src/java/javax/websocket/OnOpen.java \
+ src/java/javax/websocket/PongMessage.java \
+ src/java/javax/websocket/RemoteEndpoint.java \
+ src/java/javax/websocket/SendHandler.java \
+ src/java/javax/websocket/SendResult.java \
+ src/java/javax/websocket/Session.java \
+ src/java/javax/websocket/SessionException.java \
+ src/java/javax/websocket/WebSocketContainer.java \
+ src/java/javax/websocket/server/DefaultServerEndpointConfig.java \
+ src/java/javax/websocket/server/HandshakeRequest.java \
+ src/java/javax/websocket/server/PathParam.java \
+ src/java/javax/websocket/server/ServerApplicationConfig.java \
+ src/java/javax/websocket/server/ServerContainer.java \
+ src/java/javax/websocket/server/ServerEndpoint.java \
+ src/java/javax/websocket/server/ServerEndpointConfig.java \
"
cat << END >> $NXT_MAKEFILE
@@ -378,7 +486,8 @@ cat << END >> $NXT_MAKEFILE
all: ${NXT_JAVA_MODULE}
${NXT_JAVA_MODULE}: $NXT_BUILD_DIR/${NXT_JAVA_MODULE}.unit.so \
- $NXT_BUILD_DIR/$NXT_UNIT_JAR
+ $NXT_BUILD_DIR/$NXT_UNIT_JAR \
+ $NXT_BUILD_DIR/$NXT_WS_API_JAR
$NXT_BUILD_DIR/${NXT_JAVA_MODULE}.unit.so: $nxt_objs
\$(NXT_MODULE_LINK) -o $NXT_BUILD_DIR/${NXT_JAVA_MODULE}.unit.so \\
@@ -388,12 +497,15 @@ $NXT_BUILD_DIR/${NXT_JAVA_MODULE}.unit.so: $nxt_objs
install: ${NXT_JAVA_MODULE}-install
${NXT_JAVA_MODULE}-install: $NXT_BUILD_DIR/${NXT_JAVA_MODULE}.unit.so \\
- $NXT_BUILD_DIR/$NXT_UNIT_JAR java-shared-install
+ $NXT_BUILD_DIR/$NXT_UNIT_JAR \\
+ $NXT_BUILD_DIR/$NXT_WS_API_JAR \\
+ java-shared-install
install -d \$(DESTDIR)$NXT_MODULES
install -p $NXT_BUILD_DIR/${NXT_JAVA_MODULE}.unit.so \\
\$(DESTDIR)$NXT_MODULES/
install -d \$(DESTDIR)$NXT_JARS
install -p -m 0644 $NXT_BUILD_DIR/$NXT_UNIT_JAR \$(DESTDIR)$NXT_JARS/
+ install -p -m 0644 $NXT_BUILD_DIR/$NXT_WS_API_JAR \$(DESTDIR)$NXT_JARS/
uninstall: ${NXT_JAVA_MODULE}-uninstall
@@ -402,6 +514,7 @@ ${NXT_JAVA_MODULE}-uninstall: java-shared-uninstall
rm -f \$(DESTDIR)$NXT_MODULES/${NXT_JAVA_MODULE}.unit.so
@rmdir -p \$(DESTDIR)$NXT_MODULES 2>/dev/null || true
rm -f \$(DESTDIR)$NXT_JARS/$NXT_UNIT_JAR
+ rm -f \$(DESTDIR)$NXT_JARS/$NXT_WS_API_JAR
@rmdir -p \$(DESTDIR)$NXT_JARS 2>/dev/null || true
END
@@ -410,14 +523,15 @@ if ! grep ^$NXT_BUILD_DIR/$NXT_UNIT_JAR: $NXT_MAKEFILE 2>&1 > /dev/null; then
cat << END >> $NXT_MAKEFILE
-.INTERMEDIATE: $NXT_BUILD_DIR/$NXT_JAVA_MODULE/.classes
-
NXT_JAVA_SRCS = $NXT_JAVA_SRCS
-$NXT_BUILD_DIR/$NXT_JAVA_MODULE/.classes: \$(NXT_JAVA_SRCS)
- rm -rf $NXT_BUILD_DIR/$NXT_JAVA_MODULE/nginx
- $NXT_JAVAC -d $NXT_BUILD_DIR/$NXT_JAVA_MODULE -cp $NXT_JAVA_BUILD_CP \\
+$NXT_BUILD_DIR/$NXT_JAVA_MODULE/.nginx.unit.classes: \$(NXT_JAVA_SRCS) \\
+ $NXT_BUILD_DIR/$NXT_WS_API_JAR
+ rm -rf $NXT_BUILD_DIR/$NXT_JAVA_MODULE/nginx/unit
+ $NXT_JAVAC -d $NXT_BUILD_DIR/$NXT_JAVA_MODULE \\
+ -cp $NXT_JAVA_BUILD_CP:$NXT_BUILD_DIR/$NXT_WS_API_JAR \\
\$(NXT_JAVA_SRCS)
+ touch \$@
$NXT_BUILD_DIR/$NXT_JAVA_MODULE/META-INF/LICENSE: LICENSE
mkdir -p $NXT_BUILD_DIR/$NXT_JAVA_MODULE/META-INF
@@ -427,15 +541,49 @@ $NXT_BUILD_DIR/$NXT_JAVA_MODULE/META-INF/NOTICE: NOTICE
mkdir -p $NXT_BUILD_DIR/$NXT_JAVA_MODULE/META-INF
cp -p NOTICE \$@
-
-$NXT_BUILD_DIR/$NXT_UNIT_JAR: $NXT_BUILD_DIR/$NXT_JAVA_MODULE/.classes \\
+$NXT_BUILD_DIR/$NXT_UNIT_JAR: \\
+ $NXT_BUILD_DIR/$NXT_JAVA_MODULE/.nginx.unit.classes \\
$NXT_BUILD_DIR/$NXT_JAVA_MODULE/META-INF/LICENSE \\
$NXT_BUILD_DIR/$NXT_JAVA_MODULE/META-INF/NOTICE
$NXT_JAVA_HOME/bin/jar c -C $NXT_BUILD_DIR/$NXT_JAVA_MODULE META-INF \\
-C $NXT_BUILD_DIR/$NXT_JAVA_MODULE nginx/unit > \$@
+NXT_JAVA_WS_API_SRCS = $NXT_JAVA_WS_API_SRCS
+
+$NXT_BUILD_DIR/$NXT_JAVA_MODULE/.javax.websocket.classes: \$(NXT_JAVA_WS_API_SRCS)
+ rm -rf $NXT_BUILD_DIR/$NXT_JAVA_MODULE/javax/websocket
+ $NXT_JAVAC -d $NXT_BUILD_DIR/$NXT_JAVA_MODULE -cp $NXT_JAVA_BUILD_CP \\
+ \$(NXT_JAVA_WS_API_SRCS)
+ touch \$@
+
+$NXT_BUILD_DIR/$NXT_WS_API_JAR: \\
+ $NXT_BUILD_DIR/$NXT_JAVA_MODULE/.javax.websocket.classes \\
+ $NXT_BUILD_DIR/$NXT_JAVA_MODULE/META-INF/LICENSE \\
+ $NXT_BUILD_DIR/$NXT_JAVA_MODULE/META-INF/NOTICE
+ $NXT_JAVA_HOME/bin/jar c -C $NXT_BUILD_DIR/$NXT_JAVA_MODULE META-INF \\
+ -C $NXT_BUILD_DIR/$NXT_JAVA_MODULE javax/websocket > \$@
+
END
+NXT_PROPS="\
+ nginx/unit/websocket/LocalStrings.properties \
+ nginx/unit/websocket/pojo/LocalStrings.properties \
+ nginx/unit/websocket/server/LocalStrings.properties \
+"
+
+for nxt_prop in $NXT_PROPS ; do
+
+ cat << END >> $NXT_MAKEFILE
+$NXT_BUILD_DIR/$NXT_JAVA_MODULE/$nxt_prop: src/java/$nxt_prop \\
+ $NXT_BUILD_DIR/$NXT_JAVA_MODULE/.nginx.unit.classes
+ cp src/java/$nxt_prop \$@
+
+$NXT_BUILD_DIR/$NXT_UNIT_JAR: $NXT_BUILD_DIR/$NXT_JAVA_MODULE/$nxt_prop
+
+END
+
+done
+
fi
if ! grep ^java-shared-install: $NXT_MAKEFILE 2>&1 > /dev/null; then
diff --git a/auto/sources b/auto/sources
index 8ac8fb19..c4b3808b 100644
--- a/auto/sources
+++ b/auto/sources
@@ -71,6 +71,7 @@ NXT_LIB_SRCS=" \
src/nxt_upstream_round_robin.c \
src/nxt_http_parse.c \
src/nxt_app_log.c \
+ src/nxt_capability.c \
src/nxt_runtime.c \
src/nxt_conf.c \
src/nxt_conf_validation.c \
@@ -83,6 +84,7 @@ NXT_LIB_SRCS=" \
src/nxt_http_response.c \
src/nxt_http_error.c \
src/nxt_http_route.c \
+ src/nxt_http_static.c \
src/nxt_application.c \
src/nxt_external.c \
src/nxt_port_hash.c \
@@ -132,6 +134,7 @@ NXT_LIB_SOLARIS_SENDFILEV_SRCS="src/nxt_solaris_sendfilev.c"
NXT_LIB_MACOSX_SENDFILE_SRCS="src/nxt_macosx_sendfile.c"
NXT_LIB_AIX_SEND_FILE_SRCS="src/nxt_aix_send_file.c"
NXT_LIB_HPUX_SENDFILE_SRCS="src/nxt_hpux_sendfile.c"
+NXT_LIB_CLONE_SRCS="src/nxt_clone.c"
NXT_TEST_BUILD_DEPS="src/nxt_test_build.h"
NXT_TEST_BUILD_SRCS="src/nxt_test_build.c"
@@ -257,6 +260,11 @@ if [ "$NXT_HAVE_HPUX_SENDFILE" = "YES" \
fi
+if [ "$NXT_HAVE_CLONE" = "YES" ]; then
+ NXT_LIB_SRCS="$NXT_LIB_SRCS $NXT_LIB_CLONE_SRCS"
+fi
+
+
if [ "$NXT_TEST_BUILD" = "YES" ]; then
NXT_LIB_SRCS="$NXT_LIB_SRCS $NXT_TEST_BUILD_SRCS"
fi
diff --git a/auto/summary b/auto/summary
index 1c9df4b1..59267f6c 100644
--- a/auto/summary
+++ b/auto/summary
@@ -26,6 +26,8 @@ Unit configuration summary:
Unix domain sockets support: $NXT_UNIX_DOMAIN
TLS support: ............... $NXT_OPENSSL
+ process isolation: ......... $NXT_ISOLATION
+
debug logging: ............. $NXT_DEBUG
END
diff --git a/configure b/configure
index 335a8c88..b6cd3087 100755
--- a/configure
+++ b/configure
@@ -126,6 +126,8 @@ NXT_LIBRT=
. auto/os/conf
. auto/ssltls
. auto/pcre
+. auto/isolation
+. auto/capability
case "$NXT_SYSTEM_PLATFORM" in
diff --git a/docs/changes.xml b/docs/changes.xml
index 3a47acd6..0fbea179 100644
--- a/docs/changes.xml
+++ b/docs/changes.xml
@@ -5,6 +5,71 @@
<change_log title="unit">
+<changes apply="unit-php
+ unit-python unit-python2.7
+ unit-python3.4 unit-python3.5 unit-python3.6 unit-python3.7
+ unit-go unit-go1.7 unit-go1.8 unit-go1.9 unit-go1.10 unit-go1.11
+ unit-perl
+ unit-ruby
+ unit-jsc-common unit-jsc8 unit-jsc10 unit-jsc11"
+ ver="1.11.0" rev="1"
+ date="2019-09-19" time="18:00:00 +0300"
+ packager="Konstantin Pavlov &lt;thresh@nginx.com&gt;">
+
+<change>
+<para>
+NGINX Unit updated to 1.11.0.
+</para>
+</change>
+
+</changes>
+
+
+<changes apply="unit" ver="1.11.0" rev="1"
+ date="2019-09-19" time="18:00:00 +0300"
+ packager="Konstantin Pavlov &lt;thresh@nginx.com&gt;">
+
+<change type="feature">
+<para>
+basic support for serving static files.
+</para>
+</change>
+
+<change type="feature">
+<para>
+isolation of application processes with Linux namespaces.
+</para>
+</change>
+
+<change type="feature">
+<para>
+built-in WebSocket server implementation for Java Servlet Containers.
+</para>
+</change>
+
+<change type="feature">
+<para>
+direct addressing of API configuration options containing slashes "/"
+using URI encoding (%2F).
+</para>
+</change>
+
+<change type="bugfix">
+<para>
+segmentation fault might have occurred in Go applications under high load.
+</para>
+</change>
+
+<change type="bugfix">
+<para>
+WebSocket support was broken if Unit was built with some linkers other than
+GNU ld (e.g. gold or LLD).
+</para>
+</change>
+
+</changes>
+
+
<changes apply="unit
unit-php
unit-python unit-python2.7
diff --git a/pkg/docker/Dockerfile.full b/pkg/docker/Dockerfile.full
index b2727c7b..55ad41b8 100644
--- a/pkg/docker/Dockerfile.full
+++ b/pkg/docker/Dockerfile.full
@@ -2,7 +2,7 @@ FROM debian:stretch-slim
LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"
-ENV UNIT_VERSION 1.10.0-2~stretch
+ENV UNIT_VERSION 1.11.0-1~stretch
RUN set -x \
&& apt-get update \
diff --git a/pkg/docker/Dockerfile.go1.7-dev b/pkg/docker/Dockerfile.go1.7-dev
index f45f990a..cb5a19f5 100644
--- a/pkg/docker/Dockerfile.go1.7-dev
+++ b/pkg/docker/Dockerfile.go1.7-dev
@@ -2,7 +2,7 @@ FROM debian:stretch-slim
LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"
-ENV UNIT_VERSION 1.10.0-2~stretch
+ENV UNIT_VERSION 1.11.0-1~stretch
RUN set -x \
&& apt-get update \
diff --git a/pkg/docker/Dockerfile.go1.8-dev b/pkg/docker/Dockerfile.go1.8-dev
index a4840129..36c0eb2a 100644
--- a/pkg/docker/Dockerfile.go1.8-dev
+++ b/pkg/docker/Dockerfile.go1.8-dev
@@ -2,7 +2,7 @@ FROM debian:stretch-slim
LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"
-ENV UNIT_VERSION 1.10.0-2~stretch
+ENV UNIT_VERSION 1.11.0-1~stretch
RUN set -x \
&& apt-get update \
diff --git a/pkg/docker/Dockerfile.minimal b/pkg/docker/Dockerfile.minimal
index be50f3b8..38744ed9 100644
--- a/pkg/docker/Dockerfile.minimal
+++ b/pkg/docker/Dockerfile.minimal
@@ -2,7 +2,7 @@ FROM debian:stretch-slim
LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"
-ENV UNIT_VERSION 1.10.0-2~stretch
+ENV UNIT_VERSION 1.11.0-1~stretch
RUN set -x \
&& apt-get update \
diff --git a/pkg/docker/Dockerfile.perl5.24 b/pkg/docker/Dockerfile.perl5.24
index 67cd8155..aa76f440 100644
--- a/pkg/docker/Dockerfile.perl5.24
+++ b/pkg/docker/Dockerfile.perl5.24
@@ -2,7 +2,7 @@ FROM debian:stretch-slim
LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"
-ENV UNIT_VERSION 1.10.0-2~stretch
+ENV UNIT_VERSION 1.11.0-1~stretch
RUN set -x \
&& apt-get update \
diff --git a/pkg/docker/Dockerfile.php7.0 b/pkg/docker/Dockerfile.php7.0
index 7c55f9ab..5ae8dcb4 100644
--- a/pkg/docker/Dockerfile.php7.0
+++ b/pkg/docker/Dockerfile.php7.0
@@ -2,7 +2,7 @@ FROM debian:stretch-slim
LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"
-ENV UNIT_VERSION 1.10.0-2~stretch
+ENV UNIT_VERSION 1.11.0-1~stretch
RUN set -x \
&& apt-get update \
diff --git a/pkg/docker/Dockerfile.python2.7 b/pkg/docker/Dockerfile.python2.7
index b3f09d91..f7a35109 100644
--- a/pkg/docker/Dockerfile.python2.7
+++ b/pkg/docker/Dockerfile.python2.7
@@ -2,7 +2,7 @@ FROM debian:stretch-slim
LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"
-ENV UNIT_VERSION 1.10.0-2~stretch
+ENV UNIT_VERSION 1.11.0-1~stretch
RUN set -x \
&& apt-get update \
diff --git a/pkg/docker/Dockerfile.python3.5 b/pkg/docker/Dockerfile.python3.5
index 4efd8e0c..4e27ca89 100644
--- a/pkg/docker/Dockerfile.python3.5
+++ b/pkg/docker/Dockerfile.python3.5
@@ -2,7 +2,7 @@ FROM debian:stretch-slim
LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"
-ENV UNIT_VERSION 1.10.0-2~stretch
+ENV UNIT_VERSION 1.11.0-1~stretch
RUN set -x \
&& apt-get update \
diff --git a/pkg/docker/Dockerfile.ruby2.3 b/pkg/docker/Dockerfile.ruby2.3
index 60b36ac0..35e4779c 100644
--- a/pkg/docker/Dockerfile.ruby2.3
+++ b/pkg/docker/Dockerfile.ruby2.3
@@ -2,7 +2,7 @@ FROM debian:stretch-slim
LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"
-ENV UNIT_VERSION 1.10.0-2~stretch
+ENV UNIT_VERSION 1.11.0-1~stretch
RUN set -x \
&& apt-get update \
diff --git a/pkg/rpm/Makefile.jsc-common b/pkg/rpm/Makefile.jsc-common
index 9c7e9164..68d7721d 100644
--- a/pkg/rpm/Makefile.jsc-common
+++ b/pkg/rpm/Makefile.jsc-common
@@ -6,7 +6,9 @@ MODULE_SUMMARY_jsc_common= Java shared packages for NGINX Unit
MODULE_VERSION_jsc_common= $(VERSION)
MODULE_RELEASE_jsc_common= 2
-MODULE_CONFARGS_jsc_common= java --home=/usr/lib/jvm/java-1.8.0 --jars=/usr/share/unit-jsc-common/
+JAVA_ARCH_jsc_common= $(shell /usr/lib/jvm/java-1.8.0/bin/java -XshowSettings 2>&1 | grep -F -e os.arch | sed -e 's/^.*= //')
+
+MODULE_CONFARGS_jsc_common= java --home=/usr/lib/jvm/java-1.8.0 --lib-path=/usr/lib/jvm/jre-1.8.0/lib/$(JAVA_ARCH_jsc_common)/server --jars=/usr/share/unit-jsc-common/
MODULE_MAKEARGS_jsc_common= java
MODULE_INSTARGS_jsc_common= java-shared-install
diff --git a/pkg/rpm/Makefile.jsc11 b/pkg/rpm/Makefile.jsc11
index 9f730ef2..522e0bf2 100644
--- a/pkg/rpm/Makefile.jsc11
+++ b/pkg/rpm/Makefile.jsc11
@@ -6,7 +6,7 @@ MODULE_SUMMARY_jsc11= Java 11 module for NGINX Unit
MODULE_VERSION_jsc11= $(VERSION)
MODULE_RELEASE_jsc11= 2
-MODULE_CONFARGS_jsc11= java --module=java11 --home=/usr/lib/jvm/java-11 --jars=/usr/share/unit-jsc-common/
+MODULE_CONFARGS_jsc11= java --module=java11 --home=/usr/lib/jvm/java-11 --lib-path=/usr/lib/jvm/jre-11/lib/server --jars=/usr/share/unit-jsc-common/
MODULE_MAKEARGS_jsc11= java11
MODULE_INSTARGS_jsc11= java11-install
@@ -18,6 +18,7 @@ BUILD_DEPENDS+= $(BUILD_DEPENDS_jsc11)
define MODULE_DEFINITIONS_jsc11
Requires: unit-jsc-common == $(MODULE_VERSION_jsc_common)-$(MODULE_RELEASE_jsc_common)%{?dist}.ngx
+Requires: java-11-headless
endef
export MODULE_DEFINITIONS_jsc11
diff --git a/pkg/rpm/Makefile.jsc8 b/pkg/rpm/Makefile.jsc8
index 6c45cc52..59ba9b8d 100644
--- a/pkg/rpm/Makefile.jsc8
+++ b/pkg/rpm/Makefile.jsc8
@@ -6,7 +6,9 @@ MODULE_SUMMARY_jsc8= Java 8 module for NGINX Unit
MODULE_VERSION_jsc8= $(VERSION)
MODULE_RELEASE_jsc8= 2
-MODULE_CONFARGS_jsc8= java --module=java8 --home=/usr/lib/jvm/java-1.8.0 --jars=/usr/share/unit-jsc-common/
+JAVA_ARCH_jsc8= $(shell /usr/lib/jvm/java-1.8.0/bin/java -XshowSettings 2>&1 | grep -F -e os.arch | sed -e 's/^.*= //')
+
+MODULE_CONFARGS_jsc8= java --module=java8 --home=/usr/lib/jvm/java-1.8.0 --lib-path=/usr/lib/jvm/jre-1.8.0/lib/$(JAVA_ARCH_jsc8)/server --jars=/usr/share/unit-jsc-common/
MODULE_MAKEARGS_jsc8= java8
MODULE_INSTARGS_jsc8= java8-install
@@ -18,6 +20,7 @@ BUILD_DEPENDS+= $(BUILD_DEPENDS_jsc8)
define MODULE_DEFINITIONS_jsc8
Requires: unit-jsc-common == $(MODULE_VERSION_jsc_common)-$(MODULE_RELEASE_jsc_common)%{?dist}.ngx
+Requires: java-1.8.0-headless
endef
export MODULE_DEFINITIONS_jsc8
diff --git a/src/go/unit/nxt_cgo_lib.c b/src/go/unit/nxt_cgo_lib.c
index cc1228f5..5cb31b5a 100644
--- a/src/go/unit/nxt_cgo_lib.c
+++ b/src/go/unit/nxt_cgo_lib.c
@@ -6,7 +6,6 @@
#include "_cgo_export.h"
-#include <nxt_main.h>
#include <nxt_unit.h>
#include <nxt_unit_request.h>
@@ -39,7 +38,7 @@ nxt_cgo_run(uintptr_t handler)
init.data = (void *) handler;
ctx = nxt_unit_init(&init);
- if (nxt_slow_path(ctx == NULL)) {
+ if (ctx == NULL) {
return NXT_UNIT_ERROR;
}
@@ -171,7 +170,7 @@ nxt_cgo_response_write(uintptr_t req, uintptr_t start, uint32_t len)
rc = nxt_unit_response_write((nxt_unit_request_info_t *) req,
(void *) start, len);
- if (nxt_slow_path(rc != NXT_UNIT_OK)) {
+ if (rc != NXT_UNIT_OK) {
return -1;
}
diff --git a/src/go/unit/request.go b/src/go/unit/request.go
index ad56cabb..1d8c6702 100644
--- a/src/go/unit/request.go
+++ b/src/go/unit/request.go
@@ -135,7 +135,7 @@ func nxt_go_request_set_tls(go_req uintptr) {
//export nxt_go_request_handler
func nxt_go_request_handler(go_req uintptr, h uintptr) {
r := get_request(go_req)
- handler := *(*http.Handler)(unsafe.Pointer(h))
+ handler := get_handler(h)
go func(r *request) {
handler.ServeHTTP(r.response(), &r.req)
diff --git a/src/go/unit/unit.go b/src/go/unit/unit.go
index 06257768..1534479e 100644
--- a/src/go/unit/unit.go
+++ b/src/go/unit/unit.go
@@ -13,6 +13,7 @@ import "C"
import (
"fmt"
"net/http"
+ "sync"
"unsafe"
)
@@ -87,12 +88,58 @@ func nxt_go_warn(format string, args ...interface{}) {
C.nxt_cgo_warn(str_ref(str), C.uint32_t(len(str)))
}
+type handler_registry struct {
+ sync.RWMutex
+ next uintptr
+ m map[uintptr]*http.Handler
+}
+
+var handler_registry_ handler_registry
+
+func set_handler(handler *http.Handler) uintptr {
+
+ handler_registry_.Lock()
+ if handler_registry_.m == nil {
+ handler_registry_.m = make(map[uintptr]*http.Handler)
+ handler_registry_.next = 1
+ }
+
+ h := handler_registry_.next
+ handler_registry_.next += 1
+ handler_registry_.m[h] = handler
+
+ handler_registry_.Unlock()
+
+ return h
+}
+
+func get_handler(h uintptr) http.Handler {
+ handler_registry_.RLock()
+ defer handler_registry_.RUnlock()
+
+ return *handler_registry_.m[h]
+}
+
+func reset_handler(h uintptr) {
+
+ handler_registry_.Lock()
+ if handler_registry_.m != nil {
+ delete(handler_registry_.m, h)
+ }
+
+ handler_registry_.Unlock()
+}
+
func ListenAndServe(addr string, handler http.Handler) error {
if handler == nil {
handler = http.DefaultServeMux
}
- rc := C.nxt_cgo_run(C.uintptr_t(uintptr(unsafe.Pointer(&handler))))
+ h := set_handler(&handler)
+
+ rc := C.nxt_cgo_run(C.uintptr_t(h))
+
+ reset_handler(h)
if rc != 0 {
return http.ListenAndServe(addr, handler)
diff --git a/src/java/javax/websocket/ClientEndpoint.java b/src/java/javax/websocket/ClientEndpoint.java
new file mode 100644
index 00000000..ee984171
--- /dev/null
+++ b/src/java/javax/websocket/ClientEndpoint.java
@@ -0,0 +1,34 @@
+/*
+ * 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 javax.websocket;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import javax.websocket.ClientEndpointConfig.Configurator;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ClientEndpoint {
+ String[] subprotocols() default {};
+ Class<? extends Decoder>[] decoders() default {};
+ Class<? extends Encoder>[] encoders() default {};
+ public Class<? extends Configurator> configurator()
+ default Configurator.class;
+}
diff --git a/src/java/javax/websocket/ClientEndpointConfig.java b/src/java/javax/websocket/ClientEndpointConfig.java
new file mode 100644
index 00000000..13b6cba5
--- /dev/null
+++ b/src/java/javax/websocket/ClientEndpointConfig.java
@@ -0,0 +1,138 @@
+/*
+ * 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 javax.websocket;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public interface ClientEndpointConfig extends EndpointConfig {
+
+ List<String> getPreferredSubprotocols();
+
+ List<Extension> getExtensions();
+
+ public Configurator getConfigurator();
+
+ public final class Builder {
+
+ private static final Configurator DEFAULT_CONFIGURATOR =
+ new Configurator() {};
+
+
+ public static Builder create() {
+ return new Builder();
+ }
+
+
+ private Builder() {
+ // Hide default constructor
+ }
+
+ private Configurator configurator = DEFAULT_CONFIGURATOR;
+ private List<String> preferredSubprotocols = Collections.emptyList();
+ private List<Extension> extensions = Collections.emptyList();
+ private List<Class<? extends Encoder>> encoders =
+ Collections.emptyList();
+ private List<Class<? extends Decoder>> decoders =
+ Collections.emptyList();
+
+
+ public ClientEndpointConfig build() {
+ return new DefaultClientEndpointConfig(preferredSubprotocols,
+ extensions, encoders, decoders, configurator);
+ }
+
+
+ public Builder configurator(Configurator configurator) {
+ if (configurator == null) {
+ this.configurator = DEFAULT_CONFIGURATOR;
+ } else {
+ this.configurator = configurator;
+ }
+ return this;
+ }
+
+
+ public Builder preferredSubprotocols(
+ List<String> preferredSubprotocols) {
+ if (preferredSubprotocols == null ||
+ preferredSubprotocols.size() == 0) {
+ this.preferredSubprotocols = Collections.emptyList();
+ } else {
+ this.preferredSubprotocols =
+ Collections.unmodifiableList(preferredSubprotocols);
+ }
+ return this;
+ }
+
+
+ public Builder extensions(
+ List<Extension> extensions) {
+ if (extensions == null || extensions.size() == 0) {
+ this.extensions = Collections.emptyList();
+ } else {
+ this.extensions = Collections.unmodifiableList(extensions);
+ }
+ return this;
+ }
+
+
+ public Builder encoders(List<Class<? extends Encoder>> encoders) {
+ if (encoders == null || encoders.size() == 0) {
+ this.encoders = Collections.emptyList();
+ } else {
+ this.encoders = Collections.unmodifiableList(encoders);
+ }
+ return this;
+ }
+
+
+ public Builder decoders(List<Class<? extends Decoder>> decoders) {
+ if (decoders == null || decoders.size() == 0) {
+ this.decoders = Collections.emptyList();
+ } else {
+ this.decoders = Collections.unmodifiableList(decoders);
+ }
+ return this;
+ }
+ }
+
+
+ public class Configurator {
+
+ /**
+ * Provides the client with a mechanism to inspect and/or modify the headers
+ * that are sent to the server to start the WebSocket handshake.
+ *
+ * @param headers The HTTP headers
+ */
+ public void beforeRequest(Map<String, List<String>> headers) {
+ // NO-OP
+ }
+
+ /**
+ * Provides the client with a mechanism to inspect the handshake response
+ * that is returned from the server.
+ *
+ * @param handshakeResponse The response
+ */
+ public void afterResponse(HandshakeResponse handshakeResponse) {
+ // NO-OP
+ }
+ }
+}
diff --git a/src/java/javax/websocket/CloseReason.java b/src/java/javax/websocket/CloseReason.java
new file mode 100644
index 00000000..ef88d135
--- /dev/null
+++ b/src/java/javax/websocket/CloseReason.java
@@ -0,0 +1,122 @@
+/*
+ * 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 javax.websocket;
+
+public class CloseReason {
+
+ private final CloseCode closeCode;
+ private final String reasonPhrase;
+
+ public CloseReason(CloseReason.CloseCode closeCode, String reasonPhrase) {
+ this.closeCode = closeCode;
+ this.reasonPhrase = reasonPhrase;
+ }
+
+ public CloseCode getCloseCode() {
+ return closeCode;
+ }
+
+ public String getReasonPhrase() {
+ return reasonPhrase;
+ }
+
+ @Override
+ public String toString() {
+ return "CloseReason: code [" + closeCode.getCode() +
+ "], reason [" + reasonPhrase + "]";
+ }
+
+ public interface CloseCode {
+ int getCode();
+ }
+
+ public enum CloseCodes implements CloseReason.CloseCode {
+
+ NORMAL_CLOSURE(1000),
+ GOING_AWAY(1001),
+ PROTOCOL_ERROR(1002),
+ CANNOT_ACCEPT(1003),
+ RESERVED(1004),
+ NO_STATUS_CODE(1005),
+ CLOSED_ABNORMALLY(1006),
+ NOT_CONSISTENT(1007),
+ VIOLATED_POLICY(1008),
+ TOO_BIG(1009),
+ NO_EXTENSION(1010),
+ UNEXPECTED_CONDITION(1011),
+ SERVICE_RESTART(1012),
+ TRY_AGAIN_LATER(1013),
+ TLS_HANDSHAKE_FAILURE(1015);
+
+ private int code;
+
+ CloseCodes(int code) {
+ this.code = code;
+ }
+
+ public static CloseCode getCloseCode(final int code) {
+ if (code > 2999 && code < 5000) {
+ return new CloseCode() {
+ @Override
+ public int getCode() {
+ return code;
+ }
+ };
+ }
+ switch (code) {
+ case 1000:
+ return CloseCodes.NORMAL_CLOSURE;
+ case 1001:
+ return CloseCodes.GOING_AWAY;
+ case 1002:
+ return CloseCodes.PROTOCOL_ERROR;
+ case 1003:
+ return CloseCodes.CANNOT_ACCEPT;
+ case 1004:
+ return CloseCodes.RESERVED;
+ case 1005:
+ return CloseCodes.NO_STATUS_CODE;
+ case 1006:
+ return CloseCodes.CLOSED_ABNORMALLY;
+ case 1007:
+ return CloseCodes.NOT_CONSISTENT;
+ case 1008:
+ return CloseCodes.VIOLATED_POLICY;
+ case 1009:
+ return CloseCodes.TOO_BIG;
+ case 1010:
+ return CloseCodes.NO_EXTENSION;
+ case 1011:
+ return CloseCodes.UNEXPECTED_CONDITION;
+ case 1012:
+ return CloseCodes.SERVICE_RESTART;
+ case 1013:
+ return CloseCodes.TRY_AGAIN_LATER;
+ case 1015:
+ return CloseCodes.TLS_HANDSHAKE_FAILURE;
+ default:
+ throw new IllegalArgumentException(
+ "Invalid close code: [" + code + "]");
+ }
+ }
+
+ @Override
+ public int getCode() {
+ return code;
+ }
+ }
+}
diff --git a/src/java/javax/websocket/ContainerProvider.java b/src/java/javax/websocket/ContainerProvider.java
new file mode 100644
index 00000000..1727ca93
--- /dev/null
+++ b/src/java/javax/websocket/ContainerProvider.java
@@ -0,0 +1,63 @@
+/*
+ * 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 javax.websocket;
+
+import java.util.Iterator;
+import java.util.ServiceLoader;
+
+/**
+ * Use the {@link ServiceLoader} mechanism to provide instances of the WebSocket
+ * client container.
+ */
+public abstract class ContainerProvider {
+
+ private static final String DEFAULT_PROVIDER_CLASS_NAME =
+ "nginx.unit.websocket.WsWebSocketContainer";
+
+ /**
+ * Create a new container used to create outgoing WebSocket connections.
+ *
+ * @return A newly created container.
+ */
+ public static WebSocketContainer getWebSocketContainer() {
+ WebSocketContainer result = null;
+
+ ServiceLoader<ContainerProvider> serviceLoader =
+ ServiceLoader.load(ContainerProvider.class);
+ Iterator<ContainerProvider> iter = serviceLoader.iterator();
+ while (result == null && iter.hasNext()) {
+ result = iter.next().getContainer();
+ }
+
+ // Fall-back. Also used by unit tests
+ if (result == null) {
+ try {
+ @SuppressWarnings("unchecked")
+ Class<WebSocketContainer> clazz =
+ (Class<WebSocketContainer>) Class.forName(
+ DEFAULT_PROVIDER_CLASS_NAME);
+ result = clazz.getConstructor().newInstance();
+ } catch (ReflectiveOperationException | IllegalArgumentException |
+ SecurityException e) {
+ // No options left. Just return null.
+ }
+ }
+ return result;
+ }
+
+ protected abstract WebSocketContainer getContainer();
+}
diff --git a/src/java/javax/websocket/DecodeException.java b/src/java/javax/websocket/DecodeException.java
new file mode 100644
index 00000000..771cfa58
--- /dev/null
+++ b/src/java/javax/websocket/DecodeException.java
@@ -0,0 +1,56 @@
+/*
+ * 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 javax.websocket;
+
+import java.nio.ByteBuffer;
+
+public class DecodeException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ private ByteBuffer bb;
+ private String encodedString;
+
+ public DecodeException(ByteBuffer bb, String message, Throwable cause) {
+ super(message, cause);
+ this.bb = bb;
+ }
+
+ public DecodeException(String encodedString, String message,
+ Throwable cause) {
+ super(message, cause);
+ this.encodedString = encodedString;
+ }
+
+ public DecodeException(ByteBuffer bb, String message) {
+ super(message);
+ this.bb = bb;
+ }
+
+ public DecodeException(String encodedString, String message) {
+ super(message);
+ this.encodedString = encodedString;
+ }
+
+ public ByteBuffer getBytes() {
+ return bb;
+ }
+
+ public String getText() {
+ return encodedString;
+ }
+}
diff --git a/src/java/javax/websocket/Decoder.java b/src/java/javax/websocket/Decoder.java
new file mode 100644
index 00000000..fad262e3
--- /dev/null
+++ b/src/java/javax/websocket/Decoder.java
@@ -0,0 +1,53 @@
+/*
+ * 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 javax.websocket;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.ByteBuffer;
+
+public interface Decoder {
+
+ abstract void init(EndpointConfig endpointConfig);
+
+ abstract void destroy();
+
+ interface Binary<T> extends Decoder {
+
+ T decode(ByteBuffer bytes) throws DecodeException;
+
+ boolean willDecode(ByteBuffer bytes);
+ }
+
+ interface BinaryStream<T> extends Decoder {
+
+ T decode(InputStream is) throws DecodeException, IOException;
+ }
+
+ interface Text<T> extends Decoder {
+
+ T decode(String s) throws DecodeException;
+
+ boolean willDecode(String s);
+ }
+
+ interface TextStream<T> extends Decoder {
+
+ T decode(Reader reader) throws DecodeException, IOException;
+ }
+}
diff --git a/src/java/javax/websocket/DefaultClientEndpointConfig.java b/src/java/javax/websocket/DefaultClientEndpointConfig.java
new file mode 100644
index 00000000..ce28cb26
--- /dev/null
+++ b/src/java/javax/websocket/DefaultClientEndpointConfig.java
@@ -0,0 +1,80 @@
+/*
+ * 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 javax.websocket;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+final class DefaultClientEndpointConfig implements ClientEndpointConfig {
+
+ private final List<String> preferredSubprotocols;
+ private final List<Extension> extensions;
+ private final List<Class<? extends Encoder>> encoders;
+ private final List<Class<? extends Decoder>> decoders;
+ private final Map<String,Object> userProperties = new ConcurrentHashMap<>();
+ private final Configurator configurator;
+
+
+ DefaultClientEndpointConfig(List<String> preferredSubprotocols,
+ List<Extension> extensions,
+ List<Class<? extends Encoder>> encoders,
+ List<Class<? extends Decoder>> decoders,
+ Configurator configurator) {
+ this.preferredSubprotocols = preferredSubprotocols;
+ this.extensions = extensions;
+ this.decoders = decoders;
+ this.encoders = encoders;
+ this.configurator = configurator;
+ }
+
+
+ @Override
+ public List<String> getPreferredSubprotocols() {
+ return preferredSubprotocols;
+ }
+
+
+ @Override
+ public List<Extension> getExtensions() {
+ return extensions;
+ }
+
+
+ @Override
+ public List<Class<? extends Encoder>> getEncoders() {
+ return encoders;
+ }
+
+
+ @Override
+ public List<Class<? extends Decoder>> getDecoders() {
+ return decoders;
+ }
+
+
+ @Override
+ public final Map<String, Object> getUserProperties() {
+ return userProperties;
+ }
+
+
+ @Override
+ public Configurator getConfigurator() {
+ return configurator;
+ }
+}
diff --git a/src/java/javax/websocket/DeploymentException.java b/src/java/javax/websocket/DeploymentException.java
new file mode 100644
index 00000000..1678fd09
--- /dev/null
+++ b/src/java/javax/websocket/DeploymentException.java
@@ -0,0 +1,30 @@
+/*
+ * 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 javax.websocket;
+
+public class DeploymentException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public DeploymentException(String message) {
+ super(message);
+ }
+
+ public DeploymentException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/java/javax/websocket/EncodeException.java b/src/java/javax/websocket/EncodeException.java
new file mode 100644
index 00000000..fdb536ac
--- /dev/null
+++ b/src/java/javax/websocket/EncodeException.java
@@ -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 javax.websocket;
+
+public class EncodeException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ private Object object;
+
+ public EncodeException(Object object, String message) {
+ super(message);
+ this.object = object;
+ }
+
+ public EncodeException(Object object, String message, Throwable cause) {
+ super(message, cause);
+ this.object = object;
+ }
+
+ public Object getObject() {
+ return this.object;
+ }
+}
diff --git a/src/java/javax/websocket/Encoder.java b/src/java/javax/websocket/Encoder.java
new file mode 100644
index 00000000..42a107f0
--- /dev/null
+++ b/src/java/javax/websocket/Encoder.java
@@ -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 javax.websocket;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.nio.ByteBuffer;
+
+public interface Encoder {
+
+ abstract void init(EndpointConfig endpointConfig);
+
+ abstract void destroy();
+
+ interface Text<T> extends Encoder {
+
+ String encode(T object) throws EncodeException;
+ }
+
+ interface TextStream<T> extends Encoder {
+
+ void encode(T object, Writer writer)
+ throws EncodeException, IOException;
+ }
+
+ interface Binary<T> extends Encoder {
+
+ ByteBuffer encode(T object) throws EncodeException;
+ }
+
+ interface BinaryStream<T> extends Encoder {
+
+ void encode(T object, OutputStream os)
+ throws EncodeException, IOException;
+ }
+}
diff --git a/src/java/javax/websocket/Endpoint.java b/src/java/javax/websocket/Endpoint.java
new file mode 100644
index 00000000..9dfdbcce
--- /dev/null
+++ b/src/java/javax/websocket/Endpoint.java
@@ -0,0 +1,49 @@
+/*
+ * 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 javax.websocket;
+
+public abstract class Endpoint {
+
+ /**
+ * Event that is triggered when a new session starts.
+ *
+ * @param session The new session.
+ * @param config The configuration with which the Endpoint was
+ * configured.
+ */
+ public abstract void onOpen(Session session, EndpointConfig config);
+
+ /**
+ * Event that is triggered when a session has closed.
+ *
+ * @param session The session
+ * @param closeReason Why the session was closed
+ */
+ public void onClose(Session session, CloseReason closeReason) {
+ // NO-OP by default
+ }
+
+ /**
+ * Event that is triggered when a protocol error occurs.
+ *
+ * @param session The session.
+ * @param throwable The exception.
+ */
+ public void onError(Session session, Throwable throwable) {
+ // NO-OP by default
+ }
+}
diff --git a/src/java/javax/websocket/EndpointConfig.java b/src/java/javax/websocket/EndpointConfig.java
new file mode 100644
index 00000000..0b6c9681
--- /dev/null
+++ b/src/java/javax/websocket/EndpointConfig.java
@@ -0,0 +1,29 @@
+/*
+ * 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 javax.websocket;
+
+import java.util.List;
+import java.util.Map;
+
+public interface EndpointConfig {
+
+ List<Class<? extends Encoder>> getEncoders();
+
+ List<Class<? extends Decoder>> getDecoders();
+
+ Map<String,Object> getUserProperties();
+}
diff --git a/src/java/javax/websocket/Extension.java b/src/java/javax/websocket/Extension.java
new file mode 100644
index 00000000..b95b27b8
--- /dev/null
+++ b/src/java/javax/websocket/Extension.java
@@ -0,0 +1,29 @@
+/*
+ * 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 javax.websocket;
+
+import java.util.List;
+
+public interface Extension {
+ String getName();
+ List<Parameter> getParameters();
+
+ interface Parameter {
+ String getName();
+ String getValue();
+ }
+}
diff --git a/src/java/javax/websocket/HandshakeResponse.java b/src/java/javax/websocket/HandshakeResponse.java
new file mode 100644
index 00000000..807192e8
--- /dev/null
+++ b/src/java/javax/websocket/HandshakeResponse.java
@@ -0,0 +1,30 @@
+/*
+ * 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 javax.websocket;
+
+import java.util.List;
+import java.util.Map;
+
+public interface HandshakeResponse {
+
+ /**
+ * Name of the WebSocket accept HTTP header.
+ */
+ public static final String SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept";
+
+ Map<String,List<String>> getHeaders();
+}
diff --git a/src/java/javax/websocket/MessageHandler.java b/src/java/javax/websocket/MessageHandler.java
new file mode 100644
index 00000000..2c30d997
--- /dev/null
+++ b/src/java/javax/websocket/MessageHandler.java
@@ -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 javax.websocket;
+
+public interface MessageHandler {
+
+ interface Partial<T> extends MessageHandler {
+
+ /**
+ * Called when part of a message is available to be processed.
+ *
+ * @param messagePart The message part
+ * @param last <code>true</code> if this is the last part of
+ * this message, else <code>false</code>
+ */
+ void onMessage(T messagePart, boolean last);
+ }
+
+ interface Whole<T> extends MessageHandler {
+
+ /**
+ * Called when a whole message is available to be processed.
+ *
+ * @param message The message
+ */
+ void onMessage(T message);
+ }
+}
diff --git a/src/java/javax/websocket/OnClose.java b/src/java/javax/websocket/OnClose.java
new file mode 100644
index 00000000..6ee61d36
--- /dev/null
+++ b/src/java/javax/websocket/OnClose.java
@@ -0,0 +1,27 @@
+/*
+ * 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 javax.websocket;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface OnClose {
+}
diff --git a/src/java/javax/websocket/OnError.java b/src/java/javax/websocket/OnError.java
new file mode 100644
index 00000000..ce431484
--- /dev/null
+++ b/src/java/javax/websocket/OnError.java
@@ -0,0 +1,27 @@
+/*
+ * 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 javax.websocket;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface OnError {
+}
diff --git a/src/java/javax/websocket/OnMessage.java b/src/java/javax/websocket/OnMessage.java
new file mode 100644
index 00000000..564fa994
--- /dev/null
+++ b/src/java/javax/websocket/OnMessage.java
@@ -0,0 +1,28 @@
+/*
+ * 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 javax.websocket;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface OnMessage {
+ long maxMessageSize() default -1;
+}
diff --git a/src/java/javax/websocket/OnOpen.java b/src/java/javax/websocket/OnOpen.java
new file mode 100644
index 00000000..9f0ea6e3
--- /dev/null
+++ b/src/java/javax/websocket/OnOpen.java
@@ -0,0 +1,27 @@
+/*
+ * 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 javax.websocket;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface OnOpen {
+}
diff --git a/src/java/javax/websocket/PongMessage.java b/src/java/javax/websocket/PongMessage.java
new file mode 100644
index 00000000..7e9e3b6a
--- /dev/null
+++ b/src/java/javax/websocket/PongMessage.java
@@ -0,0 +1,32 @@
+/*
+ * 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 javax.websocket;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Represents a WebSocket Pong message and used by message handlers to enable
+ * applications to process the response to any Pings they send.
+ */
+public interface PongMessage {
+ /**
+ * Get the payload of the Pong message.
+ *
+ * @return The payload of the Pong message.
+ */
+ ByteBuffer getApplicationData();
+}
diff --git a/src/java/javax/websocket/RemoteEndpoint.java b/src/java/javax/websocket/RemoteEndpoint.java
new file mode 100644
index 00000000..19c7a100
--- /dev/null
+++ b/src/java/javax/websocket/RemoteEndpoint.java
@@ -0,0 +1,229 @@
+/*
+ * 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 javax.websocket;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.nio.ByteBuffer;
+import java.util.concurrent.Future;
+
+
+public interface RemoteEndpoint {
+
+ interface Async extends RemoteEndpoint {
+
+ /**
+ * Obtain the timeout (in milliseconds) for sending a message
+ * asynchronously. The default value is determined by
+ * {@link WebSocketContainer#getDefaultAsyncSendTimeout()}.
+ * @return The current send timeout in milliseconds. A non-positive
+ * value means an infinite timeout.
+ */
+ long getSendTimeout();
+
+ /**
+ * Set the timeout (in milliseconds) for sending a message
+ * asynchronously. The default value is determined by
+ * {@link WebSocketContainer#getDefaultAsyncSendTimeout()}.
+ * @param timeout The new timeout for sending messages asynchronously
+ * in milliseconds. A non-positive value means an
+ * infinite timeout.
+ */
+ void setSendTimeout(long timeout);
+
+ /**
+ * Send the message asynchronously, using the SendHandler to signal to the
+ * client when the message has been sent.
+ * @param text The text message to send
+ * @param completion Used to signal to the client when the message has
+ * been sent
+ */
+ void sendText(String text, SendHandler completion);
+
+ /**
+ * Send the message asynchronously, using the Future to signal to the
+ * client when the message has been sent.
+ * @param text The text message to send
+ * @return A Future that signals when the message has been sent.
+ */
+ Future<Void> sendText(String text);
+
+ /**
+ * Send the message asynchronously, using the Future to signal to the client
+ * when the message has been sent.
+ * @param data The text message to send
+ * @return A Future that signals when the message has been sent.
+ * @throws IllegalArgumentException if {@code data} is {@code null}.
+ */
+ Future<Void> sendBinary(ByteBuffer data);
+
+ /**
+ * Send the message asynchronously, using the SendHandler to signal to the
+ * client when the message has been sent.
+ * @param data The text message to send
+ * @param completion Used to signal to the client when the message has
+ * been sent
+ * @throws IllegalArgumentException if {@code data} or {@code completion}
+ * is {@code null}.
+ */
+ void sendBinary(ByteBuffer data, SendHandler completion);
+
+ /**
+ * Encodes object as a message and sends it asynchronously, using the
+ * Future to signal to the client when the message has been sent.
+ * @param obj The object to be sent.
+ * @return A Future that signals when the message has been sent.
+ * @throws IllegalArgumentException if {@code obj} is {@code null}.
+ */
+ Future<Void> sendObject(Object obj);
+
+ /**
+ * Encodes object as a message and sends it asynchronously, using the
+ * SendHandler to signal to the client when the message has been sent.
+ * @param obj The object to be sent.
+ * @param completion Used to signal to the client when the message has
+ * been sent
+ * @throws IllegalArgumentException if {@code obj} or
+ * {@code completion} is {@code null}.
+ */
+ void sendObject(Object obj, SendHandler completion);
+
+ }
+
+ interface Basic extends RemoteEndpoint {
+
+ /**
+ * Send the message, blocking until the message is sent.
+ * @param text The text message to send.
+ * @throws IllegalArgumentException if {@code text} is {@code null}.
+ * @throws IOException if an I/O error occurs during the sending of the
+ * message.
+ */
+ void sendText(String text) throws IOException;
+
+ /**
+ * Send the message, blocking until the message is sent.
+ * @param data The binary message to send
+ * @throws IllegalArgumentException if {@code data} is {@code null}.
+ * @throws IOException if an I/O error occurs during the sending of the
+ * message.
+ */
+ void sendBinary(ByteBuffer data) throws IOException;
+
+ /**
+ * Sends part of a text message to the remote endpoint. Once the first part
+ * of a message has been sent, no other text or binary messages may be sent
+ * until all remaining parts of this message have been sent.
+ *
+ * @param fragment The partial message to send
+ * @param isLast <code>true</code> if this is the last part of the
+ * message, otherwise <code>false</code>
+ * @throws IllegalArgumentException if {@code fragment} is {@code null}.
+ * @throws IOException if an I/O error occurs during the sending of the
+ * message.
+ */
+ void sendText(String fragment, boolean isLast) throws IOException;
+
+ /**
+ * Sends part of a binary message to the remote endpoint. Once the first
+ * part of a message has been sent, no other text or binary messages may be
+ * sent until all remaining parts of this message have been sent.
+ *
+ * @param partialByte The partial message to send
+ * @param isLast <code>true</code> if this is the last part of the
+ * message, otherwise <code>false</code>
+ * @throws IllegalArgumentException if {@code partialByte} is
+ * {@code null}.
+ * @throws IOException if an I/O error occurs during the sending of the
+ * message.
+ */
+ void sendBinary(ByteBuffer partialByte, boolean isLast) throws IOException;
+
+ OutputStream getSendStream() throws IOException;
+
+ Writer getSendWriter() throws IOException;
+
+ /**
+ * Encodes object as a message and sends it to the remote endpoint.
+ * @param data The object to be sent.
+ * @throws EncodeException if there was a problem encoding the
+ * {@code data} object as a websocket message.
+ * @throws IllegalArgumentException if {@code data} is {@code null}.
+ * @throws IOException if an I/O error occurs during the sending of the
+ * message.
+ */
+ void sendObject(Object data) throws IOException, EncodeException;
+
+ }
+ /**
+ * Enable or disable the batching of outgoing messages for this endpoint. If
+ * batching is disabled when it was previously enabled then this method will
+ * block until any currently batched messages have been written.
+ *
+ * @param batchingAllowed New setting
+ * @throws IOException If changing the value resulted in a call to
+ * {@link #flushBatch()} and that call threw an
+ * {@link IOException}.
+ */
+ void setBatchingAllowed(boolean batchingAllowed) throws IOException;
+
+ /**
+ * Obtains the current batching status of the endpoint.
+ *
+ * @return <code>true</code> if batching is enabled, otherwise
+ * <code>false</code>.
+ */
+ boolean getBatchingAllowed();
+
+ /**
+ * Flush any currently batched messages to the remote endpoint. This method
+ * will block until the flush completes.
+ *
+ * @throws IOException If an I/O error occurs while flushing
+ */
+ void flushBatch() throws IOException;
+
+ /**
+ * Send a ping message blocking until the message has been sent. Note that
+ * if a message is in the process of being sent asynchronously, this method
+ * will block until that message and this ping has been sent.
+ *
+ * @param applicationData The payload for the ping message
+ *
+ * @throws IOException If an I/O error occurs while sending the ping
+ * @throws IllegalArgumentException if the applicationData is too large for
+ * a control message (max 125 bytes)
+ */
+ void sendPing(ByteBuffer applicationData)
+ throws IOException, IllegalArgumentException;
+
+ /**
+ * Send a pong message blocking until the message has been sent. Note that
+ * if a message is in the process of being sent asynchronously, this method
+ * will block until that message and this pong has been sent.
+ *
+ * @param applicationData The payload for the pong message
+ *
+ * @throws IOException If an I/O error occurs while sending the pong
+ * @throws IllegalArgumentException if the applicationData is too large for
+ * a control message (max 125 bytes)
+ */
+ void sendPong(ByteBuffer applicationData)
+ throws IOException, IllegalArgumentException;
+}
+
diff --git a/src/java/javax/websocket/SendHandler.java b/src/java/javax/websocket/SendHandler.java
new file mode 100644
index 00000000..65b9a19a
--- /dev/null
+++ b/src/java/javax/websocket/SendHandler.java
@@ -0,0 +1,22 @@
+/*
+ * 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 javax.websocket;
+
+public interface SendHandler {
+
+ void onResult(SendResult result);
+}
diff --git a/src/java/javax/websocket/SendResult.java b/src/java/javax/websocket/SendResult.java
new file mode 100644
index 00000000..a3797d5b
--- /dev/null
+++ b/src/java/javax/websocket/SendResult.java
@@ -0,0 +1,39 @@
+/*
+ * 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 javax.websocket;
+
+public final class SendResult {
+ private final Throwable exception;
+ private final boolean ok;
+
+ public SendResult(Throwable exception) {
+ this.exception = exception;
+ this.ok = (exception == null);
+ }
+
+ public SendResult() {
+ this (null);
+ }
+
+ public Throwable getException() {
+ return exception;
+ }
+
+ public boolean isOK() {
+ return ok;
+ }
+}
diff --git a/src/java/javax/websocket/Session.java b/src/java/javax/websocket/Session.java
new file mode 100644
index 00000000..eea15e5b
--- /dev/null
+++ b/src/java/javax/websocket/Session.java
@@ -0,0 +1,193 @@
+/*
+ * 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 javax.websocket;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.URI;
+import java.security.Principal;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public interface Session extends Closeable {
+
+ /**
+ * Get the container that created this session.
+ * @return the container that created this session.
+ */
+ WebSocketContainer getContainer();
+
+ /**
+ * Registers a {@link MessageHandler} for incoming messages. Only one
+ * {@link MessageHandler} may be registered for each message type (text,
+ * binary, pong). The message type will be derived at runtime from the
+ * provided {@link MessageHandler} instance. It is not always possible to do
+ * this so it is better to use
+ * {@link #addMessageHandler(Class, javax.websocket.MessageHandler.Partial)}
+ * or
+ * {@link #addMessageHandler(Class, javax.websocket.MessageHandler.Whole)}.
+ *
+ * @param handler The message handler for a incoming message
+ *
+ * @throws IllegalStateException If a message handler has already been
+ * registered for the associated message type
+ */
+ void addMessageHandler(MessageHandler handler) throws IllegalStateException;
+
+ Set<MessageHandler> getMessageHandlers();
+
+ void removeMessageHandler(MessageHandler listener);
+
+ String getProtocolVersion();
+
+ String getNegotiatedSubprotocol();
+
+ List<Extension> getNegotiatedExtensions();
+
+ boolean isSecure();
+
+ boolean isOpen();
+
+ /**
+ * Get the idle timeout for this session.
+ * @return The current idle timeout for this session in milliseconds. Zero
+ * or negative values indicate an infinite timeout.
+ */
+ long getMaxIdleTimeout();
+
+ /**
+ * Set the idle timeout for this session.
+ * @param timeout The new idle timeout for this session in milliseconds.
+ * Zero or negative values indicate an infinite timeout.
+ */
+ void setMaxIdleTimeout(long timeout);
+
+ /**
+ * Set the current maximum buffer size for binary messages.
+ * @param max The new maximum buffer size in bytes
+ */
+ void setMaxBinaryMessageBufferSize(int max);
+
+ /**
+ * Get the current maximum buffer size for binary messages.
+ * @return The current maximum buffer size in bytes
+ */
+ int getMaxBinaryMessageBufferSize();
+
+ /**
+ * Set the maximum buffer size for text messages.
+ * @param max The new maximum buffer size in characters.
+ */
+ void setMaxTextMessageBufferSize(int max);
+
+ /**
+ * Get the maximum buffer size for text messages.
+ * @return The maximum buffer size in characters.
+ */
+ int getMaxTextMessageBufferSize();
+
+ RemoteEndpoint.Async getAsyncRemote();
+
+ RemoteEndpoint.Basic getBasicRemote();
+
+ /**
+ * Provides a unique identifier for the session. This identifier should not
+ * be relied upon to be generated from a secure random source.
+ * @return A unique identifier for the session.
+ */
+ String getId();
+
+ /**
+ * Close the connection to the remote end point using the code
+ * {@link javax.websocket.CloseReason.CloseCodes#NORMAL_CLOSURE} and an
+ * empty reason phrase.
+ *
+ * @throws IOException if an I/O error occurs while the WebSocket session is
+ * being closed.
+ */
+ @Override
+ void close() throws IOException;
+
+
+ /**
+ * Close the connection to the remote end point using the specified code
+ * and reason phrase.
+ * @param closeReason The reason the WebSocket session is being closed.
+ *
+ * @throws IOException if an I/O error occurs while the WebSocket session is
+ * being closed.
+ */
+ void close(CloseReason closeReason) throws IOException;
+
+ URI getRequestURI();
+
+ Map<String, List<String>> getRequestParameterMap();
+
+ String getQueryString();
+
+ Map<String,String> getPathParameters();
+
+ Map<String,Object> getUserProperties();
+
+ Principal getUserPrincipal();
+
+ /**
+ * Obtain the set of open sessions associated with the same local endpoint
+ * as this session.
+ *
+ * @return The set of currently open sessions for the local endpoint that
+ * this session is associated with.
+ */
+ Set<Session> getOpenSessions();
+
+ /**
+ * Registers a {@link MessageHandler} for partial incoming messages. Only
+ * one {@link MessageHandler} may be registered for each message type (text
+ * or binary, pong messages are never presented as partial messages).
+ *
+ * @param <T> The type of message that the given handler is intended
+ * for
+ * @param clazz The Class that implements T
+ * @param handler The message handler for a incoming message
+ *
+ * @throws IllegalStateException If a message handler has already been
+ * registered for the associated message type
+ *
+ * @since WebSocket 1.1
+ */
+ <T> void addMessageHandler(Class<T> clazz, MessageHandler.Partial<T> handler)
+ throws IllegalStateException;
+
+ /**
+ * Registers a {@link MessageHandler} for whole incoming messages. Only
+ * one {@link MessageHandler} may be registered for each message type (text,
+ * binary, pong).
+ *
+ * @param <T> The type of message that the given handler is intended
+ * for
+ * @param clazz The Class that implements T
+ * @param handler The message handler for a incoming message
+ *
+ * @throws IllegalStateException If a message handler has already been
+ * registered for the associated message type
+ *
+ * @since WebSocket 1.1
+ */
+ <T> void addMessageHandler(Class<T> clazz, MessageHandler.Whole<T> handler)
+ throws IllegalStateException;
+}
diff --git a/src/java/javax/websocket/SessionException.java b/src/java/javax/websocket/SessionException.java
new file mode 100644
index 00000000..428b82ec
--- /dev/null
+++ b/src/java/javax/websocket/SessionException.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package javax.websocket;
+
+public class SessionException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Session session;
+
+
+ public SessionException(String message, Throwable cause, Session session) {
+ super(message, cause);
+ this.session = session;
+ }
+
+
+ public Session getSession() {
+ return session;
+ }
+}
diff --git a/src/java/javax/websocket/WebSocketContainer.java b/src/java/javax/websocket/WebSocketContainer.java
new file mode 100644
index 00000000..f2da3e43
--- /dev/null
+++ b/src/java/javax/websocket/WebSocketContainer.java
@@ -0,0 +1,131 @@
+/*
+ * 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 javax.websocket;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Set;
+
+public interface WebSocketContainer {
+
+ /**
+ * Get the default timeout for sending a message asynchronously.
+ * @return The current default timeout in milliseconds. A non-positive value
+ * means an infinite timeout.
+ */
+ long getDefaultAsyncSendTimeout();
+
+ /**
+ * Set the default timeout for sending a message asynchronously.
+ * @param timeout The new default timeout in milliseconds. A non-positive
+ * value means an infinite timeout.
+ */
+ void setAsyncSendTimeout(long timeout);
+
+ Session connectToServer(Object endpoint, URI path)
+ throws DeploymentException, IOException;
+
+ Session connectToServer(Class<?> annotatedEndpointClass, URI path)
+ throws DeploymentException, IOException;
+
+ /**
+ * Creates a new connection to the WebSocket.
+ *
+ * @param endpoint
+ * The endpoint instance that will handle responses from the
+ * server
+ * @param clientEndpointConfiguration
+ * Used to configure the new connection
+ * @param path
+ * The full URL of the WebSocket endpoint to connect to
+ *
+ * @return The WebSocket session for the connection
+ *
+ * @throws DeploymentException If the connection cannot be established
+ * @throws IOException If an I/O occurred while trying to establish the
+ * connection
+ */
+ Session connectToServer(Endpoint endpoint,
+ ClientEndpointConfig clientEndpointConfiguration, URI path)
+ throws DeploymentException, IOException;
+
+ /**
+ * Creates a new connection to the WebSocket.
+ *
+ * @param endpoint
+ * An instance of this class will be created to handle responses
+ * from the server
+ * @param clientEndpointConfiguration
+ * Used to configure the new connection
+ * @param path
+ * The full URL of the WebSocket endpoint to connect to
+ *
+ * @return The WebSocket session for the connection
+ *
+ * @throws DeploymentException If the connection cannot be established
+ * @throws IOException If an I/O occurred while trying to establish the
+ * connection
+ */
+ Session connectToServer(Class<? extends Endpoint> endpoint,
+ ClientEndpointConfig clientEndpointConfiguration, URI path)
+ throws DeploymentException, IOException;
+
+ /**
+ * Get the current default session idle timeout.
+ * @return The current default session idle timeout in milliseconds. Zero or
+ * negative values indicate an infinite timeout.
+ */
+ long getDefaultMaxSessionIdleTimeout();
+
+ /**
+ * Set the default session idle timeout.
+ * @param timeout The new default session idle timeout in milliseconds. Zero
+ * or negative values indicate an infinite timeout.
+ */
+ void setDefaultMaxSessionIdleTimeout(long timeout);
+
+ /**
+ * Get the default maximum buffer size for binary messages.
+ * @return The current default maximum buffer size in bytes
+ */
+ int getDefaultMaxBinaryMessageBufferSize();
+
+ /**
+ * Set the default maximum buffer size for binary messages.
+ * @param max The new default maximum buffer size in bytes
+ */
+ void setDefaultMaxBinaryMessageBufferSize(int max);
+
+ /**
+ * Get the default maximum buffer size for text messages.
+ * @return The current default maximum buffer size in characters
+ */
+ int getDefaultMaxTextMessageBufferSize();
+
+ /**
+ * Set the default maximum buffer size for text messages.
+ * @param max The new default maximum buffer size in characters
+ */
+ void setDefaultMaxTextMessageBufferSize(int max);
+
+ /**
+ * Get the installed extensions.
+ * @return The set of extensions that are supported by this WebSocket
+ * implementation.
+ */
+ Set<Extension> getInstalledExtensions();
+}
diff --git a/src/java/javax/websocket/server/DefaultServerEndpointConfig.java b/src/java/javax/websocket/server/DefaultServerEndpointConfig.java
new file mode 100644
index 00000000..7c3b8d7d
--- /dev/null
+++ b/src/java/javax/websocket/server/DefaultServerEndpointConfig.java
@@ -0,0 +1,95 @@
+/*
+ * 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 javax.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;
+
+/**
+ * Provides the default configuration for WebSocket server endpoints.
+ */
+final class DefaultServerEndpointConfig implements ServerEndpointConfig {
+
+ private final Class<?> endpointClass;
+ private final String path;
+ private final List<String> subprotocols;
+ private final List<Extension> extensions;
+ private final List<Class<? extends Encoder>> encoders;
+ private final List<Class<? extends Decoder>> decoders;
+ private final Configurator serverEndpointConfigurator;
+ private final Map<String,Object> userProperties = new ConcurrentHashMap<>();
+
+ DefaultServerEndpointConfig(
+ Class<?> endpointClass, String path,
+ List<String> subprotocols, List<Extension> extensions,
+ List<Class<? extends Encoder>> encoders,
+ List<Class<? extends Decoder>> decoders,
+ Configurator serverEndpointConfigurator) {
+ this.endpointClass = endpointClass;
+ this.path = path;
+ this.subprotocols = subprotocols;
+ this.extensions = extensions;
+ this.encoders = encoders;
+ this.decoders = decoders;
+ this.serverEndpointConfigurator = serverEndpointConfigurator;
+ }
+
+ @Override
+ public Class<?> getEndpointClass() {
+ return endpointClass;
+ }
+
+ @Override
+ public List<Class<? extends Encoder>> getEncoders() {
+ return this.encoders;
+ }
+
+ @Override
+ public List<Class<? extends Decoder>> getDecoders() {
+ return this.decoders;
+ }
+
+ @Override
+ public String getPath() {
+ return path;
+ }
+
+ @Override
+ public Configurator getConfigurator() {
+ return serverEndpointConfigurator;
+ }
+
+ @Override
+ public final Map<String, Object> getUserProperties() {
+ return userProperties;
+ }
+
+ @Override
+ public final List<String> getSubprotocols() {
+ return subprotocols;
+ }
+
+ @Override
+ public final List<Extension> getExtensions() {
+ return extensions;
+ }
+}
diff --git a/src/java/javax/websocket/server/HandshakeRequest.java b/src/java/javax/websocket/server/HandshakeRequest.java
new file mode 100644
index 00000000..f2e33273
--- /dev/null
+++ b/src/java/javax/websocket/server/HandshakeRequest.java
@@ -0,0 +1,53 @@
+/*
+ * 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 javax.websocket.server;
+
+import java.net.URI;
+import java.security.Principal;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents the HTTP request that asked to be upgraded to WebSocket.
+ */
+public interface HandshakeRequest {
+
+ static final String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key";
+ static final String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol";
+ static final String SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version";
+ static final String SEC_WEBSOCKET_EXTENSIONS= "Sec-WebSocket-Extensions";
+
+ Map<String,List<String>> getHeaders();
+
+ Principal getUserPrincipal();
+
+ URI getRequestURI();
+
+ boolean isUserInRole(String role);
+
+ /**
+ * Get the HTTP Session object associated with this request. Object is used
+ * to avoid a direct dependency on the Servlet API.
+ * @return The javax.servlet.http.HttpSession object associated with this
+ * request, if any.
+ */
+ Object getHttpSession();
+
+ Map<String, List<String>> getParameterMap();
+
+ String getQueryString();
+}
diff --git a/src/java/javax/websocket/server/PathParam.java b/src/java/javax/websocket/server/PathParam.java
new file mode 100644
index 00000000..ff1d085e
--- /dev/null
+++ b/src/java/javax/websocket/server/PathParam.java
@@ -0,0 +1,33 @@
+/*
+ * 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 javax.websocket.server;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Used to annotate method parameters on POJO endpoints the the {@link
+ * ServerEndpoint} has been defined with a {@link ServerEndpoint#value()} that
+ * uses a URI template.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface PathParam {
+ String value();
+}
diff --git a/src/java/javax/websocket/server/ServerApplicationConfig.java b/src/java/javax/websocket/server/ServerApplicationConfig.java
new file mode 100644
index 00000000..b91f1c43
--- /dev/null
+++ b/src/java/javax/websocket/server/ServerApplicationConfig.java
@@ -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 javax.websocket.server;
+
+import java.util.Set;
+
+import javax.websocket.Endpoint;
+
+/**
+ * Applications may provide an implementation of this interface to filter the
+ * discovered WebSocket endpoints that are deployed. Implementations of this
+ * class will be discovered via an ServletContainerInitializer scan.
+ */
+public interface ServerApplicationConfig {
+
+ /**
+ * Enables applications to filter the discovered implementations of
+ * {@link ServerEndpointConfig}.
+ *
+ * @param scanned The {@link Endpoint} implementations found in the
+ * application
+ * @return The set of configurations for the endpoint the application
+ * wishes to deploy
+ */
+ Set<ServerEndpointConfig> getEndpointConfigs(
+ Set<Class<? extends Endpoint>> scanned);
+
+ /**
+ * Enables applications to filter the discovered classes annotated with
+ * {@link ServerEndpoint}.
+ *
+ * @param scanned The POJOs annotated with {@link ServerEndpoint} found in
+ * the application
+ * @return The set of POJOs the application wishes to deploy
+ */
+ Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scanned);
+}
diff --git a/src/java/javax/websocket/server/ServerContainer.java b/src/java/javax/websocket/server/ServerContainer.java
new file mode 100644
index 00000000..3243a07c
--- /dev/null
+++ b/src/java/javax/websocket/server/ServerContainer.java
@@ -0,0 +1,30 @@
+/*
+ * 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 javax.websocket.server;
+
+import javax.websocket.DeploymentException;
+import javax.websocket.WebSocketContainer;
+
+/**
+ * Provides the ability to deploy endpoints programmatically.
+ */
+public interface ServerContainer extends WebSocketContainer {
+ public abstract void addEndpoint(Class<?> clazz) throws DeploymentException;
+
+ public abstract void addEndpoint(ServerEndpointConfig sec)
+ throws DeploymentException;
+}
diff --git a/src/java/javax/websocket/server/ServerEndpoint.java b/src/java/javax/websocket/server/ServerEndpoint.java
new file mode 100644
index 00000000..43b7dfa2
--- /dev/null
+++ b/src/java/javax/websocket/server/ServerEndpoint.java
@@ -0,0 +1,46 @@
+/*
+ * 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 javax.websocket.server;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import javax.websocket.Decoder;
+import javax.websocket.Encoder;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ServerEndpoint {
+
+ /**
+ * URI or URI-template that the annotated class should be mapped to.
+ * @return The URI or URI-template that the annotated class should be mapped
+ * to.
+ */
+ String value();
+
+ String[] subprotocols() default {};
+
+ Class<? extends Decoder>[] decoders() default {};
+
+ Class<? extends Encoder>[] encoders() default {};
+
+ public Class<? extends ServerEndpointConfig.Configurator> configurator()
+ default ServerEndpointConfig.Configurator.class;
+}
diff --git a/src/java/javax/websocket/server/ServerEndpointConfig.java b/src/java/javax/websocket/server/ServerEndpointConfig.java
new file mode 100644
index 00000000..5afdf79c
--- /dev/null
+++ b/src/java/javax/websocket/server/ServerEndpointConfig.java
@@ -0,0 +1,218 @@
+/*
+ * 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 javax.websocket.server;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ServiceLoader;
+
+import javax.websocket.Decoder;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+import javax.websocket.Extension;
+import javax.websocket.HandshakeResponse;
+
+/**
+ * Provides configuration information for WebSocket endpoints published to a
+ * server. Applications may provide their own implementation or use
+ * {@link Builder}.
+ */
+public interface ServerEndpointConfig extends EndpointConfig {
+
+ Class<?> getEndpointClass();
+
+ /**
+ * Returns the path at which this WebSocket server endpoint has been
+ * registered. It may be a path or a level 0 URI template.
+ * @return The registered path
+ */
+ String getPath();
+
+ List<String> getSubprotocols();
+
+ List<Extension> getExtensions();
+
+ Configurator getConfigurator();
+
+
+ public final class Builder {
+
+ public static Builder create(
+ Class<?> endpointClass, String path) {
+ return new Builder(endpointClass, path);
+ }
+
+
+ private final Class<?> endpointClass;
+ private final String path;
+ private List<Class<? extends Encoder>> encoders =
+ Collections.emptyList();
+ private List<Class<? extends Decoder>> decoders =
+ Collections.emptyList();
+ private List<String> subprotocols = Collections.emptyList();
+ private List<Extension> extensions = Collections.emptyList();
+ private Configurator configurator =
+ Configurator.fetchContainerDefaultConfigurator();
+
+
+ private Builder(Class<?> endpointClass,
+ String path) {
+ this.endpointClass = endpointClass;
+ this.path = path;
+ }
+
+ public ServerEndpointConfig build() {
+ return new DefaultServerEndpointConfig(endpointClass, path,
+ subprotocols, extensions, encoders, decoders, configurator);
+ }
+
+
+ public Builder encoders(
+ List<Class<? extends Encoder>> encoders) {
+ if (encoders == null || encoders.size() == 0) {
+ this.encoders = Collections.emptyList();
+ } else {
+ this.encoders = Collections.unmodifiableList(encoders);
+ }
+ return this;
+ }
+
+
+ public Builder decoders(
+ List<Class<? extends Decoder>> decoders) {
+ if (decoders == null || decoders.size() == 0) {
+ this.decoders = Collections.emptyList();
+ } else {
+ this.decoders = Collections.unmodifiableList(decoders);
+ }
+ return this;
+ }
+
+
+ public Builder subprotocols(
+ List<String> subprotocols) {
+ if (subprotocols == null || subprotocols.size() == 0) {
+ this.subprotocols = Collections.emptyList();
+ } else {
+ this.subprotocols = Collections.unmodifiableList(subprotocols);
+ }
+ return this;
+ }
+
+
+ public Builder extensions(
+ List<Extension> extensions) {
+ if (extensions == null || extensions.size() == 0) {
+ this.extensions = Collections.emptyList();
+ } else {
+ this.extensions = Collections.unmodifiableList(extensions);
+ }
+ return this;
+ }
+
+
+ public Builder configurator(Configurator serverEndpointConfigurator) {
+ if (serverEndpointConfigurator == null) {
+ this.configurator = Configurator.fetchContainerDefaultConfigurator();
+ } else {
+ this.configurator = serverEndpointConfigurator;
+ }
+ return this;
+ }
+ }
+
+
+ public class Configurator {
+
+ private static volatile Configurator defaultImpl = null;
+ private static final Object defaultImplLock = new Object();
+
+ private static final String DEFAULT_IMPL_CLASSNAME =
+ "nginx.unit.websocket.server.DefaultServerEndpointConfigurator";
+
+ public static void setDefault(Configurator def) {
+ synchronized (defaultImplLock) {
+ defaultImpl = def;
+ }
+ }
+
+ static Configurator fetchContainerDefaultConfigurator() {
+ if (defaultImpl == null) {
+ synchronized (defaultImplLock) {
+ if (defaultImpl == null) {
+ defaultImpl = loadDefault();
+ }
+ }
+ }
+ return defaultImpl;
+ }
+
+
+ private static Configurator loadDefault() {
+ Configurator result = null;
+
+ ServiceLoader<Configurator> serviceLoader =
+ ServiceLoader.load(Configurator.class);
+
+ Iterator<Configurator> iter = serviceLoader.iterator();
+ while (result == null && iter.hasNext()) {
+ result = iter.next();
+ }
+
+ // Fall-back. Also used by unit tests
+ if (result == null) {
+ try {
+ @SuppressWarnings("unchecked")
+ Class<Configurator> clazz =
+ (Class<Configurator>) Class.forName(
+ DEFAULT_IMPL_CLASSNAME);
+ result = clazz.getConstructor().newInstance();
+ } catch (ReflectiveOperationException | IllegalArgumentException |
+ SecurityException e) {
+ // No options left. Just return null.
+ }
+ }
+ return result;
+ }
+
+ public String getNegotiatedSubprotocol(List<String> supported,
+ List<String> requested) {
+ return fetchContainerDefaultConfigurator().getNegotiatedSubprotocol(supported, requested);
+ }
+
+ public List<Extension> getNegotiatedExtensions(List<Extension> installed,
+ List<Extension> requested) {
+ return fetchContainerDefaultConfigurator().getNegotiatedExtensions(installed, requested);
+ }
+
+ public boolean checkOrigin(String originHeaderValue) {
+ return fetchContainerDefaultConfigurator().checkOrigin(originHeaderValue);
+ }
+
+ public void modifyHandshake(ServerEndpointConfig sec,
+ HandshakeRequest request, HandshakeResponse response) {
+ fetchContainerDefaultConfigurator().modifyHandshake(sec, request, response);
+ }
+
+ public <T extends Object> T getEndpointInstance(Class<T> clazz)
+ throws InstantiationException {
+ return fetchContainerDefaultConfigurator().getEndpointInstance(
+ clazz);
+ }
+ }
+}
diff --git a/src/java/nginx/unit/Context.java b/src/java/nginx/unit/Context.java
index e1482903..6fcd6018 100644
--- a/src/java/nginx/unit/Context.java
+++ b/src/java/nginx/unit/Context.java
@@ -98,10 +98,14 @@ import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionIdListener;
import javax.servlet.http.HttpSessionListener;
+import javax.websocket.server.ServerEndpoint;
+
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
+import nginx.unit.websocket.WsSession;
+
import org.eclipse.jetty.http.MimeTypes;
import org.w3c.dom.Document;
@@ -421,6 +425,9 @@ public class Context implements ServletContext, InitParams
loader_ = new AppClassLoader(urls,
Context.class.getClassLoader().getParent());
+ Class wsSession_class = WsSession.class;
+ trace("wsSession.test: " + WsSession.wsSession_test());
+
ClassLoader old = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(loader_);
@@ -429,28 +436,30 @@ public class Context implements ServletContext, InitParams
addListener(listener_classname);
}
- ScanResult scan_res = null;
+ ClassGraph classgraph = new ClassGraph()
+ //.verbose()
+ .overrideClassLoaders(loader_)
+ .ignoreParentClassLoaders()
+ .enableClassInfo()
+ .enableAnnotationInfo()
+ //.enableSystemPackages()
+ .whitelistModules("javax.*")
+ //.enableAllInfo()
+ ;
- if (!metadata_complete_) {
- ClassGraph classgraph = new ClassGraph()
- //.verbose()
- .overrideClassLoaders(loader_)
- .ignoreParentClassLoaders()
- .enableClassInfo()
- .enableAnnotationInfo()
- //.enableSystemPackages()
- .whitelistModules("javax.*")
- //.enableAllInfo()
- ;
-
- String verbose = System.getProperty("nginx.unit.context.classgraph.verbose", "").trim();
-
- if (verbose.equals("true")) {
- classgraph.verbose();
- }
+ String verbose = System.getProperty("nginx.unit.context.classgraph.verbose", "").trim();
+
+ if (verbose.equals("true")) {
+ classgraph.verbose();
+ }
+
+ ScanResult scan_res = classgraph.scan();
+
+ javax.websocket.server.ServerEndpointConfig.Configurator.setDefault(new nginx.unit.websocket.server.DefaultServerEndpointConfigurator());
- scan_res = classgraph.scan();
+ loadInitializer(new nginx.unit.websocket.server.WsSci(), scan_res);
+ if (!metadata_complete_) {
loadInitializers(scan_res);
}
@@ -1471,54 +1480,61 @@ public class Context implements ServletContext, InitParams
ServiceLoader.load(ServletContainerInitializer.class, loader_);
for (ServletContainerInitializer sci : initializers) {
+ loadInitializer(sci, scan_res);
+ }
+ }
- trace("loadInitializers: initializer: " + sci.getClass().getName());
+ private void loadInitializer(ServletContainerInitializer sci, ScanResult scan_res)
+ {
+ trace("loadInitializer: initializer: " + sci.getClass().getName());
- HandlesTypes ann = sci.getClass().getAnnotation(HandlesTypes.class);
- if (ann == null) {
- trace("loadInitializers: no HandlesTypes annotation");
- continue;
- }
+ HandlesTypes ann = sci.getClass().getAnnotation(HandlesTypes.class);
+ if (ann == null) {
+ trace("loadInitializer: no HandlesTypes annotation");
+ return;
+ }
- Class<?>[] classes = ann.value();
- if (classes == null) {
- trace("loadInitializers: no handles classes");
- continue;
- }
+ Class<?>[] classes = ann.value();
+ if (classes == null) {
+ trace("loadInitializer: no handles classes");
+ return;
+ }
- Set<Class<?>> handles_classes = new HashSet<>();
+ Set<Class<?>> handles_classes = new HashSet<>();
- for (Class<?> c : classes) {
- trace("loadInitializers: find handles: " + c.getName());
+ for (Class<?> c : classes) {
+ trace("loadInitializer: find handles: " + c.getName());
- ClassInfoList handles = c.isInterface()
+ ClassInfoList handles =
+ c.isAnnotation()
+ ? scan_res.getClassesWithAnnotation(c.getName())
+ : c.isInterface()
? scan_res.getClassesImplementing(c.getName())
: scan_res.getSubclasses(c.getName());
- for (ClassInfo ci : handles) {
- if (ci.isInterface()
- || ci.isAnnotation()
- || ci.isAbstract())
- {
- continue;
- }
-
- trace("loadInitializers: handles class: " + ci.getName());
- handles_classes.add(ci.loadClass());
+ for (ClassInfo ci : handles) {
+ if (ci.isInterface()
+ || ci.isAnnotation()
+ || ci.isAbstract())
+ {
+ return;
}
- }
- if (handles_classes.isEmpty()) {
- trace("loadInitializers: no handles implementations");
- continue;
+ trace("loadInitializer: handles class: " + ci.getName());
+ handles_classes.add(ci.loadClass());
}
+ }
- try {
- sci.onStartup(handles_classes, this);
- metadata_complete_ = true;
- } catch(Exception e) {
- System.err.println("loadInitializers: exception caught: " + e.toString());
- }
+ if (handles_classes.isEmpty()) {
+ trace("loadInitializer: no handles implementations");
+ return;
+ }
+
+ try {
+ sci.onStartup(handles_classes, this);
+ metadata_complete_ = true;
+ } catch(Exception e) {
+ System.err.println("loadInitializer: exception caught: " + e.toString());
}
}
@@ -1691,6 +1707,21 @@ public class Context implements ServletContext, InitParams
listener_classnames_.add(ci.getName());
}
+
+
+ ClassInfoList endpoints = scan_res.getClassesWithAnnotation(ServerEndpoint.class.getName());
+
+ for (ClassInfo ci : endpoints) {
+ if (ci.isInterface()
+ || ci.isAnnotation()
+ || ci.isAbstract())
+ {
+ trace("scanClasses: skip server end point: " + ci.getName());
+ continue;
+ }
+
+ trace("scanClasses: server end point: " + ci.getName());
+ }
}
public void stop() throws IOException
diff --git a/src/java/nginx/unit/Request.java b/src/java/nginx/unit/Request.java
index 98584efe..335d7980 100644
--- a/src/java/nginx/unit/Request.java
+++ b/src/java/nginx/unit/Request.java
@@ -16,6 +16,7 @@ import java.lang.StringBuffer;
import java.net.URI;
import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
@@ -65,6 +66,9 @@ import org.eclipse.jetty.http.MultiPartFormInputStream;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.MimeTypes;
+import nginx.unit.websocket.WsSession;
+import nginx.unit.websocket.WsIOException;
+
public class Request implements HttpServletRequest, DynamicPathRequest
{
private final Context context;
@@ -114,6 +118,9 @@ public class Request implements HttpServletRequest, DynamicPathRequest
private boolean request_session_id_from_url = false;
private Session session = null;
+ private WsSession wsSession = null;
+ private boolean skip_close_ws = false;
+
private final ServletRequestAttributeListener attr_listener;
public static final String BARE = "nginx.unit.request.bare";
@@ -1203,11 +1210,30 @@ public class Request implements HttpServletRequest, DynamicPathRequest
public <T extends HttpUpgradeHandler> T upgrade(
Class<T> httpUpgradeHandlerClass) throws java.io.IOException, ServletException
{
- log("upgrade: " + httpUpgradeHandlerClass.getName());
+ trace("upgrade: " + httpUpgradeHandlerClass.getName());
- return null;
+ T handler;
+
+ try {
+ handler = httpUpgradeHandlerClass.getConstructor().newInstance();
+ } catch (Exception e) {
+ throw new ServletException(e);
+ }
+
+ upgrade(req_info_ptr);
+
+ return handler;
+ }
+
+ private static native void upgrade(long req_info_ptr);
+
+ public boolean isUpgrade()
+ {
+ return isUpgrade(req_info_ptr);
}
+ private static native boolean isUpgrade(long req_info_ptr);
+
@Override
public String changeSessionId()
{
@@ -1248,5 +1274,65 @@ public class Request implements HttpServletRequest, DynamicPathRequest
public static native void trace(long req_info_ptr, String msg, int msg_len);
private static native Response getResponse(long req_info_ptr);
+
+
+ public void setWsSession(WsSession s)
+ {
+ wsSession = s;
+ }
+
+ private void processWsFrame(ByteBuffer buf, byte opCode, boolean last)
+ throws IOException
+ {
+ trace("processWsFrame: " + opCode + ", [" + buf.position() + ", " + buf.limit() + "]");
+ try {
+ wsSession.processFrame(buf, opCode, last);
+ } catch (WsIOException e) {
+ wsSession.onClose(e.getCloseReason());
+ }
+ }
+
+ private void closeWsSession()
+ {
+ trace("closeWsSession");
+ skip_close_ws = true;
+
+ wsSession.onClose();
+ }
+
+ public void sendWsFrame(ByteBuffer payload, byte opCode, boolean last,
+ long timeoutExpiry) throws IOException
+ {
+ trace("sendWsFrame: " + opCode + ", [" + payload.position() +
+ ", " + payload.limit() + "]");
+
+ if (payload.isDirect()) {
+ sendWsFrame(req_info_ptr, payload, payload.position(),
+ payload.limit() - payload.position(), opCode, last);
+ } else {
+ sendWsFrame(req_info_ptr, payload.array(), payload.position(),
+ payload.limit() - payload.position(), opCode, last);
+ }
+ }
+
+ private static native void sendWsFrame(long req_info_ptr,
+ ByteBuffer buf, int pos, int len, byte opCode, boolean last);
+
+ private static native void sendWsFrame(long req_info_ptr,
+ byte[] arr, int pos, int len, byte opCode, boolean last);
+
+
+ public void closeWs()
+ {
+ if (skip_close_ws) {
+ return;
+ }
+
+ trace("closeWs");
+
+ closeWs(req_info_ptr);
+ }
+
+ private static native void closeWs(long req_info_ptr);
}
diff --git a/src/java/nginx/unit/websocket/AsyncChannelGroupUtil.java b/src/java/nginx/unit/websocket/AsyncChannelGroupUtil.java
new file mode 100644
index 00000000..147112c1
--- /dev/null
+++ b/src/java/nginx/unit/websocket/AsyncChannelGroupUtil.java
@@ -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 nginx.unit.websocket;
+
+import java.io.IOException;
+import java.nio.channels.AsynchronousChannelGroup;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.tomcat.util.res.StringManager;
+import org.apache.tomcat.util.threads.ThreadPoolExecutor;
+
+/**
+ * This is a utility class that enables multiple {@link WsWebSocketContainer}
+ * instances to share a single {@link AsynchronousChannelGroup} while ensuring
+ * that the group is destroyed when no longer required.
+ */
+public class AsyncChannelGroupUtil {
+
+ private static final StringManager sm =
+ StringManager.getManager(AsyncChannelGroupUtil.class);
+
+ private static AsynchronousChannelGroup group = null;
+ private static int usageCount = 0;
+ private static final Object lock = new Object();
+
+
+ private AsyncChannelGroupUtil() {
+ // Hide the default constructor
+ }
+
+
+ public static AsynchronousChannelGroup register() {
+ synchronized (lock) {
+ if (usageCount == 0) {
+ group = createAsynchronousChannelGroup();
+ }
+ usageCount++;
+ return group;
+ }
+ }
+
+
+ public static void unregister() {
+ synchronized (lock) {
+ usageCount--;
+ if (usageCount == 0) {
+ group.shutdown();
+ group = null;
+ }
+ }
+ }
+
+
+ private static AsynchronousChannelGroup createAsynchronousChannelGroup() {
+ // Need to do this with the right thread context class loader else the
+ // first web app to call this will trigger a leak
+ ClassLoader original = Thread.currentThread().getContextClassLoader();
+
+ try {
+ Thread.currentThread().setContextClassLoader(
+ AsyncIOThreadFactory.class.getClassLoader());
+
+ // These are the same settings as the default
+ // AsynchronousChannelGroup
+ int initialSize = Runtime.getRuntime().availableProcessors();
+ ExecutorService executorService = new ThreadPoolExecutor(
+ 0,
+ Integer.MAX_VALUE,
+ Long.MAX_VALUE, TimeUnit.MILLISECONDS,
+ new SynchronousQueue<Runnable>(),
+ new AsyncIOThreadFactory());
+
+ try {
+ return AsynchronousChannelGroup.withCachedThreadPool(
+ executorService, initialSize);
+ } catch (IOException e) {
+ // No good reason for this to happen.
+ throw new IllegalStateException(sm.getString("asyncChannelGroup.createFail"));
+ }
+ } finally {
+ Thread.currentThread().setContextClassLoader(original);
+ }
+ }
+
+
+ private static class AsyncIOThreadFactory implements ThreadFactory {
+
+ static {
+ // Load NewThreadPrivilegedAction since newThread() will not be able
+ // to if called from an InnocuousThread.
+ // See https://bz.apache.org/bugzilla/show_bug.cgi?id=57490
+ NewThreadPrivilegedAction.load();
+ }
+
+
+ @Override
+ public Thread newThread(final Runnable r) {
+ // Create the new Thread within a doPrivileged block to ensure that
+ // the thread inherits the current ProtectionDomain which is
+ // essential to be able to use this with a Java Applet. See
+ // https://bz.apache.org/bugzilla/show_bug.cgi?id=57091
+ return AccessController.doPrivileged(new NewThreadPrivilegedAction(r));
+ }
+
+ // Non-anonymous class so that AsyncIOThreadFactory can load it
+ // explicitly
+ private static class NewThreadPrivilegedAction implements PrivilegedAction<Thread> {
+
+ private static AtomicInteger count = new AtomicInteger(0);
+
+ private final Runnable r;
+
+ public NewThreadPrivilegedAction(Runnable r) {
+ this.r = r;
+ }
+
+ @Override
+ public Thread run() {
+ Thread t = new Thread(r);
+ t.setName("WebSocketClient-AsyncIO-" + count.incrementAndGet());
+ t.setContextClassLoader(this.getClass().getClassLoader());
+ t.setDaemon(true);
+ return t;
+ }
+
+ private static void load() {
+ // NO-OP. Just provides a hook to enable the class to be loaded
+ }
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/AsyncChannelWrapper.java b/src/java/nginx/unit/websocket/AsyncChannelWrapper.java
new file mode 100644
index 00000000..060ae9cb
--- /dev/null
+++ b/src/java/nginx/unit/websocket/AsyncChannelWrapper.java
@@ -0,0 +1,47 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.nio.ByteBuffer;
+import java.nio.channels.CompletionHandler;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLException;
+
+/**
+ * This is a wrapper for a {@link java.nio.channels.AsynchronousSocketChannel}
+ * that limits the methods available thereby simplifying the process of
+ * implementing SSL/TLS support since there are fewer methods to intercept.
+ */
+public interface AsyncChannelWrapper {
+
+ Future<Integer> read(ByteBuffer dst);
+
+ <B,A extends B> void read(ByteBuffer dst, A attachment,
+ CompletionHandler<Integer,B> handler);
+
+ Future<Integer> write(ByteBuffer src);
+
+ <B,A extends B> void write(ByteBuffer[] srcs, int offset, int length,
+ long timeout, TimeUnit unit, A attachment,
+ CompletionHandler<Long,B> handler);
+
+ void close();
+
+ Future<Void> handshake() throws SSLException;
+}
diff --git a/src/java/nginx/unit/websocket/AsyncChannelWrapperNonSecure.java b/src/java/nginx/unit/websocket/AsyncChannelWrapperNonSecure.java
new file mode 100644
index 00000000..5b88bfe1
--- /dev/null
+++ b/src/java/nginx/unit/websocket/AsyncChannelWrapperNonSecure.java
@@ -0,0 +1,112 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousSocketChannel;
+import java.nio.channels.CompletionHandler;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Generally, just passes calls straight to the wrapped
+ * {@link AsynchronousSocketChannel}. In some cases exceptions may be swallowed
+ * to save them being swallowed by the calling code.
+ */
+public class AsyncChannelWrapperNonSecure implements AsyncChannelWrapper {
+
+ private static final Future<Void> NOOP_FUTURE = new NoOpFuture();
+
+ private final AsynchronousSocketChannel socketChannel;
+
+ public AsyncChannelWrapperNonSecure(
+ AsynchronousSocketChannel socketChannel) {
+ this.socketChannel = socketChannel;
+ }
+
+ @Override
+ public Future<Integer> read(ByteBuffer dst) {
+ return socketChannel.read(dst);
+ }
+
+ @Override
+ public <B,A extends B> void read(ByteBuffer dst, A attachment,
+ CompletionHandler<Integer,B> handler) {
+ socketChannel.read(dst, attachment, handler);
+ }
+
+ @Override
+ public Future<Integer> write(ByteBuffer src) {
+ return socketChannel.write(src);
+ }
+
+ @Override
+ public <B,A extends B> void write(ByteBuffer[] srcs, int offset, int length,
+ long timeout, TimeUnit unit, A attachment,
+ CompletionHandler<Long,B> handler) {
+ socketChannel.write(
+ srcs, offset, length, timeout, unit, attachment, handler);
+ }
+
+ @Override
+ public void close() {
+ try {
+ socketChannel.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+
+ @Override
+ public Future<Void> handshake() {
+ return NOOP_FUTURE;
+ }
+
+
+ private static final class NoOpFuture implements Future<Void> {
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return false;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public boolean isDone() {
+ return true;
+ }
+
+ @Override
+ public Void get() throws InterruptedException, ExecutionException {
+ return null;
+ }
+
+ @Override
+ public Void get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException,
+ TimeoutException {
+ return null;
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/AsyncChannelWrapperSecure.java b/src/java/nginx/unit/websocket/AsyncChannelWrapperSecure.java
new file mode 100644
index 00000000..21654487
--- /dev/null
+++ b/src/java/nginx/unit/websocket/AsyncChannelWrapperSecure.java
@@ -0,0 +1,578 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousSocketChannel;
+import java.nio.channels.CompletionHandler;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLEngineResult.HandshakeStatus;
+import javax.net.ssl.SSLEngineResult.Status;
+import javax.net.ssl.SSLException;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * Wraps the {@link AsynchronousSocketChannel} with SSL/TLS. This needs a lot
+ * more testing before it can be considered robust.
+ */
+public class AsyncChannelWrapperSecure implements AsyncChannelWrapper {
+
+ private final Log log =
+ LogFactory.getLog(AsyncChannelWrapperSecure.class);
+ private static final StringManager sm =
+ StringManager.getManager(AsyncChannelWrapperSecure.class);
+
+ private static final ByteBuffer DUMMY = ByteBuffer.allocate(16921);
+ private final AsynchronousSocketChannel socketChannel;
+ private final SSLEngine sslEngine;
+ private final ByteBuffer socketReadBuffer;
+ private final ByteBuffer socketWriteBuffer;
+ // One thread for read, one for write
+ private final ExecutorService executor =
+ Executors.newFixedThreadPool(2, new SecureIOThreadFactory());
+ private AtomicBoolean writing = new AtomicBoolean(false);
+ private AtomicBoolean reading = new AtomicBoolean(false);
+
+ public AsyncChannelWrapperSecure(AsynchronousSocketChannel socketChannel,
+ SSLEngine sslEngine) {
+ this.socketChannel = socketChannel;
+ this.sslEngine = sslEngine;
+
+ int socketBufferSize = sslEngine.getSession().getPacketBufferSize();
+ socketReadBuffer = ByteBuffer.allocateDirect(socketBufferSize);
+ socketWriteBuffer = ByteBuffer.allocateDirect(socketBufferSize);
+ }
+
+ @Override
+ public Future<Integer> read(ByteBuffer dst) {
+ WrapperFuture<Integer,Void> future = new WrapperFuture<>();
+
+ if (!reading.compareAndSet(false, true)) {
+ throw new IllegalStateException(sm.getString(
+ "asyncChannelWrapperSecure.concurrentRead"));
+ }
+
+ ReadTask readTask = new ReadTask(dst, future);
+
+ executor.execute(readTask);
+
+ return future;
+ }
+
+ @Override
+ public <B,A extends B> void read(ByteBuffer dst, A attachment,
+ CompletionHandler<Integer,B> handler) {
+
+ WrapperFuture<Integer,B> future =
+ new WrapperFuture<>(handler, attachment);
+
+ if (!reading.compareAndSet(false, true)) {
+ throw new IllegalStateException(sm.getString(
+ "asyncChannelWrapperSecure.concurrentRead"));
+ }
+
+ ReadTask readTask = new ReadTask(dst, future);
+
+ executor.execute(readTask);
+ }
+
+ @Override
+ public Future<Integer> write(ByteBuffer src) {
+
+ WrapperFuture<Long,Void> inner = new WrapperFuture<>();
+
+ if (!writing.compareAndSet(false, true)) {
+ throw new IllegalStateException(sm.getString(
+ "asyncChannelWrapperSecure.concurrentWrite"));
+ }
+
+ WriteTask writeTask =
+ new WriteTask(new ByteBuffer[] {src}, 0, 1, inner);
+
+ executor.execute(writeTask);
+
+ Future<Integer> future = new LongToIntegerFuture(inner);
+ return future;
+ }
+
+ @Override
+ public <B,A extends B> void write(ByteBuffer[] srcs, int offset, int length,
+ long timeout, TimeUnit unit, A attachment,
+ CompletionHandler<Long,B> handler) {
+
+ WrapperFuture<Long,B> future =
+ new WrapperFuture<>(handler, attachment);
+
+ if (!writing.compareAndSet(false, true)) {
+ throw new IllegalStateException(sm.getString(
+ "asyncChannelWrapperSecure.concurrentWrite"));
+ }
+
+ WriteTask writeTask = new WriteTask(srcs, offset, length, future);
+
+ executor.execute(writeTask);
+ }
+
+ @Override
+ public void close() {
+ try {
+ socketChannel.close();
+ } catch (IOException e) {
+ log.info(sm.getString("asyncChannelWrapperSecure.closeFail"));
+ }
+ executor.shutdownNow();
+ }
+
+ @Override
+ public Future<Void> handshake() throws SSLException {
+
+ WrapperFuture<Void,Void> wFuture = new WrapperFuture<>();
+
+ Thread t = new WebSocketSslHandshakeThread(wFuture);
+ t.start();
+
+ return wFuture;
+ }
+
+
+ private class WriteTask implements Runnable {
+
+ private final ByteBuffer[] srcs;
+ private final int offset;
+ private final int length;
+ private final WrapperFuture<Long,?> future;
+
+ public WriteTask(ByteBuffer[] srcs, int offset, int length,
+ WrapperFuture<Long,?> future) {
+ this.srcs = srcs;
+ this.future = future;
+ this.offset = offset;
+ this.length = length;
+ }
+
+ @Override
+ public void run() {
+ long written = 0;
+
+ try {
+ for (int i = offset; i < offset + length; i++) {
+ ByteBuffer src = srcs[i];
+ while (src.hasRemaining()) {
+ socketWriteBuffer.clear();
+
+ // Encrypt the data
+ SSLEngineResult r = sslEngine.wrap(src, socketWriteBuffer);
+ written += r.bytesConsumed();
+ Status s = r.getStatus();
+
+ if (s == Status.OK || s == Status.BUFFER_OVERFLOW) {
+ // Need to write out the bytes and may need to read from
+ // the source again to empty it
+ } else {
+ // Status.BUFFER_UNDERFLOW - only happens on unwrap
+ // Status.CLOSED - unexpected
+ throw new IllegalStateException(sm.getString(
+ "asyncChannelWrapperSecure.statusWrap"));
+ }
+
+ // Check for tasks
+ if (r.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
+ Runnable runnable = sslEngine.getDelegatedTask();
+ while (runnable != null) {
+ runnable.run();
+ runnable = sslEngine.getDelegatedTask();
+ }
+ }
+
+ socketWriteBuffer.flip();
+
+ // Do the write
+ int toWrite = r.bytesProduced();
+ while (toWrite > 0) {
+ Future<Integer> f =
+ socketChannel.write(socketWriteBuffer);
+ Integer socketWrite = f.get();
+ toWrite -= socketWrite.intValue();
+ }
+ }
+ }
+
+
+ if (writing.compareAndSet(true, false)) {
+ future.complete(Long.valueOf(written));
+ } else {
+ future.fail(new IllegalStateException(sm.getString(
+ "asyncChannelWrapperSecure.wrongStateWrite")));
+ }
+ } catch (Exception e) {
+ writing.set(false);
+ future.fail(e);
+ }
+ }
+ }
+
+
+ private class ReadTask implements Runnable {
+
+ private final ByteBuffer dest;
+ private final WrapperFuture<Integer,?> future;
+
+ public ReadTask(ByteBuffer dest, WrapperFuture<Integer,?> future) {
+ this.dest = dest;
+ this.future = future;
+ }
+
+ @Override
+ public void run() {
+ int read = 0;
+
+ boolean forceRead = false;
+
+ try {
+ while (read == 0) {
+ socketReadBuffer.compact();
+
+ if (forceRead) {
+ forceRead = false;
+ Future<Integer> f = socketChannel.read(socketReadBuffer);
+ Integer socketRead = f.get();
+ if (socketRead.intValue() == -1) {
+ throw new EOFException(sm.getString("asyncChannelWrapperSecure.eof"));
+ }
+ }
+
+ socketReadBuffer.flip();
+
+ if (socketReadBuffer.hasRemaining()) {
+ // Decrypt the data in the buffer
+ SSLEngineResult r = sslEngine.unwrap(socketReadBuffer, dest);
+ read += r.bytesProduced();
+ Status s = r.getStatus();
+
+ if (s == Status.OK) {
+ // Bytes available for reading and there may be
+ // sufficient data in the socketReadBuffer to
+ // support further reads without reading from the
+ // socket
+ } else if (s == Status.BUFFER_UNDERFLOW) {
+ // There is partial data in the socketReadBuffer
+ if (read == 0) {
+ // Need more data before the partial data can be
+ // processed and some output generated
+ forceRead = true;
+ }
+ // else return the data we have and deal with the
+ // partial data on the next read
+ } else if (s == Status.BUFFER_OVERFLOW) {
+ // Not enough space in the destination buffer to
+ // store all of the data. We could use a bytes read
+ // value of -bufferSizeRequired to signal the new
+ // buffer size required but an explicit exception is
+ // clearer.
+ if (reading.compareAndSet(true, false)) {
+ throw new ReadBufferOverflowException(sslEngine.
+ getSession().getApplicationBufferSize());
+ } else {
+ future.fail(new IllegalStateException(sm.getString(
+ "asyncChannelWrapperSecure.wrongStateRead")));
+ }
+ } else {
+ // Status.CLOSED - unexpected
+ throw new IllegalStateException(sm.getString(
+ "asyncChannelWrapperSecure.statusUnwrap"));
+ }
+
+ // Check for tasks
+ if (r.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
+ Runnable runnable = sslEngine.getDelegatedTask();
+ while (runnable != null) {
+ runnable.run();
+ runnable = sslEngine.getDelegatedTask();
+ }
+ }
+ } else {
+ forceRead = true;
+ }
+ }
+
+
+ if (reading.compareAndSet(true, false)) {
+ future.complete(Integer.valueOf(read));
+ } else {
+ future.fail(new IllegalStateException(sm.getString(
+ "asyncChannelWrapperSecure.wrongStateRead")));
+ }
+ } catch (RuntimeException | ReadBufferOverflowException | SSLException | EOFException |
+ ExecutionException | InterruptedException e) {
+ reading.set(false);
+ future.fail(e);
+ }
+ }
+ }
+
+
+ private class WebSocketSslHandshakeThread extends Thread {
+
+ private final WrapperFuture<Void,Void> hFuture;
+
+ private HandshakeStatus handshakeStatus;
+ private Status resultStatus;
+
+ public WebSocketSslHandshakeThread(WrapperFuture<Void,Void> hFuture) {
+ this.hFuture = hFuture;
+ }
+
+ @Override
+ public void run() {
+ try {
+ sslEngine.beginHandshake();
+ // So the first compact does the right thing
+ socketReadBuffer.position(socketReadBuffer.limit());
+
+ handshakeStatus = sslEngine.getHandshakeStatus();
+ resultStatus = Status.OK;
+
+ boolean handshaking = true;
+
+ while(handshaking) {
+ switch (handshakeStatus) {
+ case NEED_WRAP: {
+ socketWriteBuffer.clear();
+ SSLEngineResult r =
+ sslEngine.wrap(DUMMY, socketWriteBuffer);
+ checkResult(r, true);
+ socketWriteBuffer.flip();
+ Future<Integer> fWrite =
+ socketChannel.write(socketWriteBuffer);
+ fWrite.get();
+ break;
+ }
+ case NEED_UNWRAP: {
+ socketReadBuffer.compact();
+ if (socketReadBuffer.position() == 0 ||
+ resultStatus == Status.BUFFER_UNDERFLOW) {
+ Future<Integer> fRead =
+ socketChannel.read(socketReadBuffer);
+ fRead.get();
+ }
+ socketReadBuffer.flip();
+ SSLEngineResult r =
+ sslEngine.unwrap(socketReadBuffer, DUMMY);
+ checkResult(r, false);
+ break;
+ }
+ case NEED_TASK: {
+ Runnable r = null;
+ while ((r = sslEngine.getDelegatedTask()) != null) {
+ r.run();
+ }
+ handshakeStatus = sslEngine.getHandshakeStatus();
+ break;
+ }
+ case FINISHED: {
+ handshaking = false;
+ break;
+ }
+ case NOT_HANDSHAKING: {
+ throw new SSLException(
+ sm.getString("asyncChannelWrapperSecure.notHandshaking"));
+ }
+ }
+ }
+ } catch (Exception e) {
+ hFuture.fail(e);
+ return;
+ }
+
+ hFuture.complete(null);
+ }
+
+ private void checkResult(SSLEngineResult result, boolean wrap)
+ throws SSLException {
+
+ handshakeStatus = result.getHandshakeStatus();
+ resultStatus = result.getStatus();
+
+ if (resultStatus != Status.OK &&
+ (wrap || resultStatus != Status.BUFFER_UNDERFLOW)) {
+ throw new SSLException(
+ sm.getString("asyncChannelWrapperSecure.check.notOk", resultStatus));
+ }
+ if (wrap && result.bytesConsumed() != 0) {
+ throw new SSLException(sm.getString("asyncChannelWrapperSecure.check.wrap"));
+ }
+ if (!wrap && result.bytesProduced() != 0) {
+ throw new SSLException(sm.getString("asyncChannelWrapperSecure.check.unwrap"));
+ }
+ }
+ }
+
+
+ private static class WrapperFuture<T,A> implements Future<T> {
+
+ private final CompletionHandler<T,A> handler;
+ private final A attachment;
+
+ private volatile T result = null;
+ private volatile Throwable throwable = null;
+ private CountDownLatch completionLatch = new CountDownLatch(1);
+
+ public WrapperFuture() {
+ this(null, null);
+ }
+
+ public WrapperFuture(CompletionHandler<T,A> handler, A attachment) {
+ this.handler = handler;
+ this.attachment = attachment;
+ }
+
+ public void complete(T result) {
+ this.result = result;
+ completionLatch.countDown();
+ if (handler != null) {
+ handler.completed(result, attachment);
+ }
+ }
+
+ public void fail(Throwable t) {
+ throwable = t;
+ completionLatch.countDown();
+ if (handler != null) {
+ handler.failed(throwable, attachment);
+ }
+ }
+
+ @Override
+ public final boolean cancel(boolean mayInterruptIfRunning) {
+ // Could support cancellation by closing the connection
+ return false;
+ }
+
+ @Override
+ public final boolean isCancelled() {
+ // Could support cancellation by closing the connection
+ return false;
+ }
+
+ @Override
+ public final boolean isDone() {
+ return completionLatch.getCount() > 0;
+ }
+
+ @Override
+ public T get() throws InterruptedException, ExecutionException {
+ completionLatch.await();
+ if (throwable != null) {
+ throw new ExecutionException(throwable);
+ }
+ return result;
+ }
+
+ @Override
+ public T get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException,
+ TimeoutException {
+ boolean latchResult = completionLatch.await(timeout, unit);
+ if (latchResult == false) {
+ throw new TimeoutException();
+ }
+ if (throwable != null) {
+ throw new ExecutionException(throwable);
+ }
+ return result;
+ }
+ }
+
+ private static final class LongToIntegerFuture implements Future<Integer> {
+
+ private final Future<Long> wrapped;
+
+ public LongToIntegerFuture(Future<Long> wrapped) {
+ this.wrapped = wrapped;
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return wrapped.cancel(mayInterruptIfRunning);
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return wrapped.isCancelled();
+ }
+
+ @Override
+ public boolean isDone() {
+ return wrapped.isDone();
+ }
+
+ @Override
+ public Integer get() throws InterruptedException, ExecutionException {
+ Long result = wrapped.get();
+ if (result.longValue() > Integer.MAX_VALUE) {
+ throw new ExecutionException(sm.getString(
+ "asyncChannelWrapperSecure.tooBig", result), null);
+ }
+ return Integer.valueOf(result.intValue());
+ }
+
+ @Override
+ public Integer get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException,
+ TimeoutException {
+ Long result = wrapped.get(timeout, unit);
+ if (result.longValue() > Integer.MAX_VALUE) {
+ throw new ExecutionException(sm.getString(
+ "asyncChannelWrapperSecure.tooBig", result), null);
+ }
+ return Integer.valueOf(result.intValue());
+ }
+ }
+
+
+ private static class SecureIOThreadFactory implements ThreadFactory {
+
+ private AtomicInteger count = new AtomicInteger(0);
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r);
+ t.setName("WebSocketClient-SecureIO-" + count.incrementAndGet());
+ // No need to set the context class loader. The threads will be
+ // cleaned up when the connection is closed.
+ t.setDaemon(true);
+ return t;
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/AuthenticationException.java b/src/java/nginx/unit/websocket/AuthenticationException.java
new file mode 100644
index 00000000..001f1829
--- /dev/null
+++ b/src/java/nginx/unit/websocket/AuthenticationException.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package nginx.unit.websocket;
+
+/**
+ * Exception thrown on authentication error connecting to a remote
+ * websocket endpoint.
+ */
+public class AuthenticationException extends Exception {
+
+ private static final long serialVersionUID = 5709887412240096441L;
+
+ /**
+ * Create authentication exception.
+ * @param message the error message
+ */
+ public AuthenticationException(String message) {
+ super(message);
+ }
+
+}
diff --git a/src/java/nginx/unit/websocket/Authenticator.java b/src/java/nginx/unit/websocket/Authenticator.java
new file mode 100644
index 00000000..87b3ce6d
--- /dev/null
+++ b/src/java/nginx/unit/websocket/Authenticator.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package nginx.unit.websocket;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Base class for the authentication methods used by the websocket client.
+ */
+public abstract class Authenticator {
+ private static final Pattern pattern = Pattern
+ .compile("(\\w+)\\s*=\\s*(\"([^\"]+)\"|([^,=\"]+))\\s*,?");
+
+ /**
+ * Generate the authentication header that will be sent to the server.
+ * @param requestUri The request URI
+ * @param WWWAuthenticate The server auth challenge
+ * @param UserProperties The user information
+ * @return The auth header
+ * @throws AuthenticationException When an error occurs
+ */
+ public abstract String getAuthorization(String requestUri, String WWWAuthenticate,
+ Map<String, Object> UserProperties) throws AuthenticationException;
+
+ /**
+ * Get the authentication method.
+ * @return the auth scheme
+ */
+ public abstract String getSchemeName();
+
+ /**
+ * Utility method to parse the authentication header.
+ * @param WWWAuthenticate The server auth challenge
+ * @return the parsed header
+ */
+ public Map<String, String> parseWWWAuthenticateHeader(String WWWAuthenticate) {
+
+ Matcher m = pattern.matcher(WWWAuthenticate);
+ Map<String, String> challenge = new HashMap<>();
+
+ while (m.find()) {
+ String key = m.group(1);
+ String qtedValue = m.group(3);
+ String value = m.group(4);
+
+ challenge.put(key, qtedValue != null ? qtedValue : value);
+
+ }
+
+ return challenge;
+
+ }
+
+}
diff --git a/src/java/nginx/unit/websocket/AuthenticatorFactory.java b/src/java/nginx/unit/websocket/AuthenticatorFactory.java
new file mode 100644
index 00000000..7d46d7f9
--- /dev/null
+++ b/src/java/nginx/unit/websocket/AuthenticatorFactory.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package nginx.unit.websocket;
+
+import java.util.Iterator;
+import java.util.ServiceLoader;
+
+/**
+ * Utility method to return the appropriate authenticator according to
+ * the scheme that the server uses.
+ */
+public class AuthenticatorFactory {
+
+ /**
+ * Return a new authenticator instance.
+ * @param authScheme The scheme used
+ * @return the authenticator
+ */
+ public static Authenticator getAuthenticator(String authScheme) {
+
+ Authenticator auth = null;
+ switch (authScheme.toLowerCase()) {
+
+ case BasicAuthenticator.schemeName:
+ auth = new BasicAuthenticator();
+ break;
+
+ case DigestAuthenticator.schemeName:
+ auth = new DigestAuthenticator();
+ break;
+
+ default:
+ auth = loadAuthenticators(authScheme);
+ break;
+ }
+
+ return auth;
+
+ }
+
+ private static Authenticator loadAuthenticators(String authScheme) {
+ ServiceLoader<Authenticator> serviceLoader = ServiceLoader.load(Authenticator.class);
+ Iterator<Authenticator> auths = serviceLoader.iterator();
+
+ while (auths.hasNext()) {
+ Authenticator auth = auths.next();
+ if (auth.getSchemeName().equalsIgnoreCase(authScheme))
+ return auth;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/java/nginx/unit/websocket/BackgroundProcess.java b/src/java/nginx/unit/websocket/BackgroundProcess.java
new file mode 100644
index 00000000..0d2e1288
--- /dev/null
+++ b/src/java/nginx/unit/websocket/BackgroundProcess.java
@@ -0,0 +1,26 @@
+/*
+ * 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 nginx.unit.websocket;
+
+public interface BackgroundProcess {
+
+ void backgroundProcess();
+
+ void setProcessPeriod(int period);
+
+ int getProcessPeriod();
+}
diff --git a/src/java/nginx/unit/websocket/BackgroundProcessManager.java b/src/java/nginx/unit/websocket/BackgroundProcessManager.java
new file mode 100644
index 00000000..d8b1b950
--- /dev/null
+++ b/src/java/nginx/unit/websocket/BackgroundProcessManager.java
@@ -0,0 +1,149 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.ExceptionUtils;
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * Provides a background processing mechanism that triggers roughly once a
+ * second. The class maintains a thread that only runs when there is at least
+ * one instance of {@link BackgroundProcess} registered.
+ */
+public class BackgroundProcessManager {
+
+ private final Log log =
+ LogFactory.getLog(BackgroundProcessManager.class);
+ private static final StringManager sm =
+ StringManager.getManager(BackgroundProcessManager.class);
+ private static final BackgroundProcessManager instance;
+
+
+ static {
+ instance = new BackgroundProcessManager();
+ }
+
+
+ public static BackgroundProcessManager getInstance() {
+ return instance;
+ }
+
+ private final Set<BackgroundProcess> processes = new HashSet<>();
+ private final Object processesLock = new Object();
+ private WsBackgroundThread wsBackgroundThread = null;
+
+ private BackgroundProcessManager() {
+ // Hide default constructor
+ }
+
+
+ public void register(BackgroundProcess process) {
+ synchronized (processesLock) {
+ if (processes.size() == 0) {
+ wsBackgroundThread = new WsBackgroundThread(this);
+ wsBackgroundThread.setContextClassLoader(
+ this.getClass().getClassLoader());
+ wsBackgroundThread.setDaemon(true);
+ wsBackgroundThread.start();
+ }
+ processes.add(process);
+ }
+ }
+
+
+ public void unregister(BackgroundProcess process) {
+ synchronized (processesLock) {
+ processes.remove(process);
+ if (wsBackgroundThread != null && processes.size() == 0) {
+ wsBackgroundThread.halt();
+ wsBackgroundThread = null;
+ }
+ }
+ }
+
+
+ private void process() {
+ Set<BackgroundProcess> currentProcesses = new HashSet<>();
+ synchronized (processesLock) {
+ currentProcesses.addAll(processes);
+ }
+ for (BackgroundProcess process : currentProcesses) {
+ try {
+ process.backgroundProcess();
+ } catch (Throwable t) {
+ ExceptionUtils.handleThrowable(t);
+ log.error(sm.getString(
+ "backgroundProcessManager.processFailed"), t);
+ }
+ }
+ }
+
+
+ /*
+ * For unit testing.
+ */
+ int getProcessCount() {
+ synchronized (processesLock) {
+ return processes.size();
+ }
+ }
+
+
+ void shutdown() {
+ synchronized (processesLock) {
+ processes.clear();
+ if (wsBackgroundThread != null) {
+ wsBackgroundThread.halt();
+ wsBackgroundThread = null;
+ }
+ }
+ }
+
+
+ private static class WsBackgroundThread extends Thread {
+
+ private final BackgroundProcessManager manager;
+ private volatile boolean running = true;
+
+ public WsBackgroundThread(BackgroundProcessManager manager) {
+ setName("WebSocket background processing");
+ this.manager = manager;
+ }
+
+ @Override
+ public void run() {
+ while (running) {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ manager.process();
+ }
+ }
+
+ public void halt() {
+ setName("WebSocket background processing - stopping");
+ running = false;
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/BasicAuthenticator.java b/src/java/nginx/unit/websocket/BasicAuthenticator.java
new file mode 100644
index 00000000..1b1a6b83
--- /dev/null
+++ b/src/java/nginx/unit/websocket/BasicAuthenticator.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package nginx.unit.websocket;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+
+/**
+ * Authenticator supporting the BASIC auth method.
+ */
+public class BasicAuthenticator extends Authenticator {
+
+ public static final String schemeName = "basic";
+ public static final String charsetparam = "charset";
+
+ @Override
+ public String getAuthorization(String requestUri, String WWWAuthenticate,
+ Map<String, Object> userProperties) throws AuthenticationException {
+
+ String userName = (String) userProperties.get(Constants.WS_AUTHENTICATION_USER_NAME);
+ String password = (String) userProperties.get(Constants.WS_AUTHENTICATION_PASSWORD);
+
+ if (userName == null || password == null) {
+ throw new AuthenticationException(
+ "Failed to perform Basic authentication due to missing user/password");
+ }
+
+ Map<String, String> wwwAuthenticate = parseWWWAuthenticateHeader(WWWAuthenticate);
+
+ String userPass = userName + ":" + password;
+ Charset charset;
+
+ if (wwwAuthenticate.get(charsetparam) != null
+ && wwwAuthenticate.get(charsetparam).equalsIgnoreCase("UTF-8")) {
+ charset = StandardCharsets.UTF_8;
+ } else {
+ charset = StandardCharsets.ISO_8859_1;
+ }
+
+ String base64 = Base64.getEncoder().encodeToString(userPass.getBytes(charset));
+
+ return " Basic " + base64;
+ }
+
+ @Override
+ public String getSchemeName() {
+ return schemeName;
+ }
+
+}
diff --git a/src/java/nginx/unit/websocket/Constants.java b/src/java/nginx/unit/websocket/Constants.java
new file mode 100644
index 00000000..38b22fe0
--- /dev/null
+++ b/src/java/nginx/unit/websocket/Constants.java
@@ -0,0 +1,158 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.websocket.Extension;
+
+/**
+ * Internal implementation constants.
+ */
+public class Constants {
+
+ // OP Codes
+ public static final byte OPCODE_CONTINUATION = 0x00;
+ public static final byte OPCODE_TEXT = 0x01;
+ public static final byte OPCODE_BINARY = 0x02;
+ public static final byte OPCODE_CLOSE = 0x08;
+ public static final byte OPCODE_PING = 0x09;
+ public static final byte OPCODE_PONG = 0x0A;
+
+ // Internal OP Codes
+ // RFC 6455 limits OP Codes to 4 bits so these should never clash
+ // Always set bit 4 so these will be treated as control codes
+ static final byte INTERNAL_OPCODE_FLUSH = 0x18;
+
+ // Buffers
+ static final int DEFAULT_BUFFER_SIZE = Integer.getInteger(
+ "nginx.unit.websocket.DEFAULT_BUFFER_SIZE", 8 * 1024)
+ .intValue();
+
+ // Client connection
+ /**
+ * Property name to set to configure the value that is passed to
+ * {@link javax.net.ssl.SSLEngine#setEnabledProtocols(String[])}. The value
+ * should be a comma separated string.
+ */
+ public static final String SSL_PROTOCOLS_PROPERTY =
+ "nginx.unit.websocket.SSL_PROTOCOLS";
+ public static final String SSL_TRUSTSTORE_PROPERTY =
+ "nginx.unit.websocket.SSL_TRUSTSTORE";
+ public static final String SSL_TRUSTSTORE_PWD_PROPERTY =
+ "nginx.unit.websocket.SSL_TRUSTSTORE_PWD";
+ public static final String SSL_TRUSTSTORE_PWD_DEFAULT = "changeit";
+ /**
+ * Property name to set to configure used SSLContext. The value should be an
+ * instance of SSLContext. If this property is present, the SSL_TRUSTSTORE*
+ * properties are ignored.
+ */
+ public static final String SSL_CONTEXT_PROPERTY =
+ "nginx.unit.websocket.SSL_CONTEXT";
+ /**
+ * Property name to set to configure the timeout (in milliseconds) when
+ * establishing a WebSocket connection to server. The default is
+ * {@link #IO_TIMEOUT_MS_DEFAULT}.
+ */
+ public static final String IO_TIMEOUT_MS_PROPERTY =
+ "nginx.unit.websocket.IO_TIMEOUT_MS";
+ public static final long IO_TIMEOUT_MS_DEFAULT = 5000;
+
+ // RFC 2068 recommended a limit of 5
+ // Most browsers have a default limit of 20
+ public static final String MAX_REDIRECTIONS_PROPERTY =
+ "nginx.unit.websocket.MAX_REDIRECTIONS";
+ public static final int MAX_REDIRECTIONS_DEFAULT = 20;
+
+ // HTTP upgrade header names and values
+ public static final String HOST_HEADER_NAME = "Host";
+ public static final String UPGRADE_HEADER_NAME = "Upgrade";
+ public static final String UPGRADE_HEADER_VALUE = "websocket";
+ public static final String ORIGIN_HEADER_NAME = "Origin";
+ public static final String CONNECTION_HEADER_NAME = "Connection";
+ public static final String CONNECTION_HEADER_VALUE = "upgrade";
+ public static final String LOCATION_HEADER_NAME = "Location";
+ public static final String AUTHORIZATION_HEADER_NAME = "Authorization";
+ public static final String WWW_AUTHENTICATE_HEADER_NAME = "WWW-Authenticate";
+ public static final String WS_VERSION_HEADER_NAME = "Sec-WebSocket-Version";
+ public static final String WS_VERSION_HEADER_VALUE = "13";
+ public static final String WS_KEY_HEADER_NAME = "Sec-WebSocket-Key";
+ public static final String WS_PROTOCOL_HEADER_NAME = "Sec-WebSocket-Protocol";
+ public static final String WS_EXTENSIONS_HEADER_NAME = "Sec-WebSocket-Extensions";
+
+ /// HTTP redirection status codes
+ public static final int MULTIPLE_CHOICES = 300;
+ public static final int MOVED_PERMANENTLY = 301;
+ public static final int FOUND = 302;
+ public static final int SEE_OTHER = 303;
+ public static final int USE_PROXY = 305;
+ public static final int TEMPORARY_REDIRECT = 307;
+
+ // Configuration for Origin header in client
+ static final String DEFAULT_ORIGIN_HEADER_VALUE =
+ System.getProperty("nginx.unit.websocket.DEFAULT_ORIGIN_HEADER_VALUE");
+
+ // Configuration for blocking sends
+ public static final String BLOCKING_SEND_TIMEOUT_PROPERTY =
+ "nginx.unit.websocket.BLOCKING_SEND_TIMEOUT";
+ // Milliseconds so this is 20 seconds
+ public static final long DEFAULT_BLOCKING_SEND_TIMEOUT = 20 * 1000;
+
+ // Configuration for background processing checks intervals
+ static final int DEFAULT_PROCESS_PERIOD = Integer.getInteger(
+ "nginx.unit.websocket.DEFAULT_PROCESS_PERIOD", 10)
+ .intValue();
+
+ public static final String WS_AUTHENTICATION_USER_NAME = "nginx.unit.websocket.WS_AUTHENTICATION_USER_NAME";
+ public static final String WS_AUTHENTICATION_PASSWORD = "nginx.unit.websocket.WS_AUTHENTICATION_PASSWORD";
+
+ /* Configuration for extensions
+ * Note: These options are primarily present to enable this implementation
+ * to pass compliance tests. They are expected to be removed once
+ * the WebSocket API includes a mechanism for adding custom extensions
+ * and disabling built-in extensions.
+ */
+ static final boolean DISABLE_BUILTIN_EXTENSIONS =
+ Boolean.getBoolean("nginx.unit.websocket.DISABLE_BUILTIN_EXTENSIONS");
+ static final boolean ALLOW_UNSUPPORTED_EXTENSIONS =
+ Boolean.getBoolean("nginx.unit.websocket.ALLOW_UNSUPPORTED_EXTENSIONS");
+
+ // Configuration for stream behavior
+ static final boolean STREAMS_DROP_EMPTY_MESSAGES =
+ Boolean.getBoolean("nginx.unit.websocket.STREAMS_DROP_EMPTY_MESSAGES");
+
+ public static final boolean STRICT_SPEC_COMPLIANCE =
+ Boolean.getBoolean("nginx.unit.websocket.STRICT_SPEC_COMPLIANCE");
+
+ public static final List<Extension> INSTALLED_EXTENSIONS;
+
+ static {
+ if (DISABLE_BUILTIN_EXTENSIONS) {
+ INSTALLED_EXTENSIONS = Collections.unmodifiableList(new ArrayList<Extension>());
+ } else {
+ List<Extension> installed = new ArrayList<>(1);
+ installed.add(new WsExtension("permessage-deflate"));
+ INSTALLED_EXTENSIONS = Collections.unmodifiableList(installed);
+ }
+ }
+
+ private Constants() {
+ // Hide default constructor
+ }
+}
diff --git a/src/java/nginx/unit/websocket/DecoderEntry.java b/src/java/nginx/unit/websocket/DecoderEntry.java
new file mode 100644
index 00000000..36112ef4
--- /dev/null
+++ b/src/java/nginx/unit/websocket/DecoderEntry.java
@@ -0,0 +1,39 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import javax.websocket.Decoder;
+
+public class DecoderEntry {
+
+ private final Class<?> clazz;
+ private final Class<? extends Decoder> decoderClazz;
+
+ public DecoderEntry(Class<?> clazz,
+ Class<? extends Decoder> decoderClazz) {
+ this.clazz = clazz;
+ this.decoderClazz = decoderClazz;
+ }
+
+ public Class<?> getClazz() {
+ return clazz;
+ }
+
+ public Class<? extends Decoder> getDecoderClazz() {
+ return decoderClazz;
+ }
+}
diff --git a/src/java/nginx/unit/websocket/DigestAuthenticator.java b/src/java/nginx/unit/websocket/DigestAuthenticator.java
new file mode 100644
index 00000000..9530c303
--- /dev/null
+++ b/src/java/nginx/unit/websocket/DigestAuthenticator.java
@@ -0,0 +1,150 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Map;
+
+import org.apache.tomcat.util.security.MD5Encoder;
+
+/**
+ * Authenticator supporting the DIGEST auth method.
+ */
+public class DigestAuthenticator extends Authenticator {
+
+ public static final String schemeName = "digest";
+ private SecureRandom cnonceGenerator;
+ private int nonceCount = 0;
+ private long cNonce;
+
+ @Override
+ public String getAuthorization(String requestUri, String WWWAuthenticate,
+ Map<String, Object> userProperties) throws AuthenticationException {
+
+ String userName = (String) userProperties.get(Constants.WS_AUTHENTICATION_USER_NAME);
+ String password = (String) userProperties.get(Constants.WS_AUTHENTICATION_PASSWORD);
+
+ if (userName == null || password == null) {
+ throw new AuthenticationException(
+ "Failed to perform Digest authentication due to missing user/password");
+ }
+
+ Map<String, String> wwwAuthenticate = parseWWWAuthenticateHeader(WWWAuthenticate);
+
+ String realm = wwwAuthenticate.get("realm");
+ String nonce = wwwAuthenticate.get("nonce");
+ String messageQop = wwwAuthenticate.get("qop");
+ String algorithm = wwwAuthenticate.get("algorithm") == null ? "MD5"
+ : wwwAuthenticate.get("algorithm");
+ String opaque = wwwAuthenticate.get("opaque");
+
+ StringBuilder challenge = new StringBuilder();
+
+ if (!messageQop.isEmpty()) {
+ if (cnonceGenerator == null) {
+ cnonceGenerator = new SecureRandom();
+ }
+
+ cNonce = cnonceGenerator.nextLong();
+ nonceCount++;
+ }
+
+ challenge.append("Digest ");
+ challenge.append("username =\"" + userName + "\",");
+ challenge.append("realm=\"" + realm + "\",");
+ challenge.append("nonce=\"" + nonce + "\",");
+ challenge.append("uri=\"" + requestUri + "\",");
+
+ try {
+ challenge.append("response=\"" + calculateRequestDigest(requestUri, userName, password,
+ realm, nonce, messageQop, algorithm) + "\",");
+ }
+
+ catch (NoSuchAlgorithmException e) {
+ throw new AuthenticationException(
+ "Unable to generate request digest " + e.getMessage());
+ }
+
+ challenge.append("algorithm=" + algorithm + ",");
+ challenge.append("opaque=\"" + opaque + "\",");
+
+ if (!messageQop.isEmpty()) {
+ challenge.append("qop=\"" + messageQop + "\"");
+ challenge.append(",cnonce=\"" + cNonce + "\",");
+ challenge.append("nc=" + String.format("%08X", Integer.valueOf(nonceCount)));
+ }
+
+ return challenge.toString();
+
+ }
+
+ private String calculateRequestDigest(String requestUri, String userName, String password,
+ String realm, String nonce, String qop, String algorithm)
+ throws NoSuchAlgorithmException {
+
+ StringBuilder preDigest = new StringBuilder();
+ String A1;
+
+ if (algorithm.equalsIgnoreCase("MD5"))
+ A1 = userName + ":" + realm + ":" + password;
+
+ else
+ A1 = encodeMD5(userName + ":" + realm + ":" + password) + ":" + nonce + ":" + cNonce;
+
+ /*
+ * If the "qop" value is "auth-int", then A2 is: A2 = Method ":"
+ * digest-uri-value ":" H(entity-body) since we do not have an entity-body, A2 =
+ * Method ":" digest-uri-value for auth and auth_int
+ */
+ String A2 = "GET:" + requestUri;
+
+ preDigest.append(encodeMD5(A1));
+ preDigest.append(":");
+ preDigest.append(nonce);
+
+ if (qop.toLowerCase().contains("auth")) {
+ preDigest.append(":");
+ preDigest.append(String.format("%08X", Integer.valueOf(nonceCount)));
+ preDigest.append(":");
+ preDigest.append(String.valueOf(cNonce));
+ preDigest.append(":");
+ preDigest.append(qop);
+ }
+
+ preDigest.append(":");
+ preDigest.append(encodeMD5(A2));
+
+ return encodeMD5(preDigest.toString());
+
+ }
+
+ private String encodeMD5(String value) throws NoSuchAlgorithmException {
+ byte[] bytesOfMessage = value.getBytes(StandardCharsets.ISO_8859_1);
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ byte[] thedigest = md.digest(bytesOfMessage);
+
+ return MD5Encoder.encode(thedigest);
+ }
+
+ @Override
+ public String getSchemeName() {
+ return schemeName;
+ }
+}
diff --git a/src/java/nginx/unit/websocket/FutureToSendHandler.java b/src/java/nginx/unit/websocket/FutureToSendHandler.java
new file mode 100644
index 00000000..4a0809cb
--- /dev/null
+++ b/src/java/nginx/unit/websocket/FutureToSendHandler.java
@@ -0,0 +1,112 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.websocket.SendHandler;
+import javax.websocket.SendResult;
+
+import org.apache.tomcat.util.res.StringManager;
+
+
+/**
+ * Converts a Future to a SendHandler.
+ */
+class FutureToSendHandler implements Future<Void>, SendHandler {
+
+ private static final StringManager sm = StringManager.getManager(FutureToSendHandler.class);
+
+ private final CountDownLatch latch = new CountDownLatch(1);
+ private final WsSession wsSession;
+ private volatile AtomicReference<SendResult> result = new AtomicReference<>(null);
+
+ public FutureToSendHandler(WsSession wsSession) {
+ this.wsSession = wsSession;
+ }
+
+
+ // --------------------------------------------------------- SendHandler
+
+ @Override
+ public void onResult(SendResult result) {
+ this.result.compareAndSet(null, result);
+ latch.countDown();
+ }
+
+
+ // -------------------------------------------------------------- Future
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ // Cancelling the task is not supported
+ return false;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ // Cancelling the task is not supported
+ return false;
+ }
+
+ @Override
+ public boolean isDone() {
+ return latch.getCount() == 0;
+ }
+
+ @Override
+ public Void get() throws InterruptedException,
+ ExecutionException {
+ try {
+ wsSession.registerFuture(this);
+ latch.await();
+ } finally {
+ wsSession.unregisterFuture(this);
+ }
+ if (result.get().getException() != null) {
+ throw new ExecutionException(result.get().getException());
+ }
+ return null;
+ }
+
+ @Override
+ public Void get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException,
+ TimeoutException {
+ boolean retval = false;
+ try {
+ wsSession.registerFuture(this);
+ retval = latch.await(timeout, unit);
+ } finally {
+ wsSession.unregisterFuture(this);
+
+ }
+ if (retval == false) {
+ throw new TimeoutException(sm.getString("futureToSendHandler.timeout",
+ Long.valueOf(timeout), unit.toString().toLowerCase()));
+ }
+ if (result.get().getException() != null) {
+ throw new ExecutionException(result.get().getException());
+ }
+ return null;
+ }
+}
diff --git a/src/java/nginx/unit/websocket/LocalStrings.properties b/src/java/nginx/unit/websocket/LocalStrings.properties
new file mode 100644
index 00000000..aeafe082
--- /dev/null
+++ b/src/java/nginx/unit/websocket/LocalStrings.properties
@@ -0,0 +1,147 @@
+# 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.
+
+asyncChannelGroup.createFail=Unable to create dedicated AsynchronousChannelGroup for WebSocket clients which is required to prevent memory leaks in complex class loader environments like JavaEE containers
+
+asyncChannelWrapperSecure.closeFail=Failed to close channel cleanly
+asyncChannelWrapperSecure.check.notOk=TLS handshake returned an unexpected status [{0}]
+asyncChannelWrapperSecure.check.unwrap=Bytes were written to the output during a read
+asyncChannelWrapperSecure.check.wrap=Bytes were consumed from the input during a write
+asyncChannelWrapperSecure.concurrentRead=Concurrent read operations are not permitted
+asyncChannelWrapperSecure.concurrentWrite=Concurrent write operations are not permitted
+asyncChannelWrapperSecure.eof=Unexpected end of stream
+asyncChannelWrapperSecure.notHandshaking=Unexpected state [NOT_HANDSHAKING] during TLS handshake
+asyncChannelWrapperSecure.readOverflow=Buffer overflow. [{0}] bytes to write into a [{1}] byte buffer that already contained [{2}] bytes.
+asyncChannelWrapperSecure.statusUnwrap=Unexpected Status of SSLEngineResult after an unwrap() operation
+asyncChannelWrapperSecure.statusWrap=Unexpected Status of SSLEngineResult after a wrap() operation
+asyncChannelWrapperSecure.tooBig=The result [{0}] is too big to be expressed as an Integer
+asyncChannelWrapperSecure.wrongStateRead=Flag that indicates a read is in progress was found to be false (it should have been true) when trying to complete a read operation
+asyncChannelWrapperSecure.wrongStateWrite=Flag that indicates a write is in progress was found to be false (it should have been true) when trying to complete a write operation
+
+backgroundProcessManager.processFailed=A background process failed
+
+caseInsensitiveKeyMap.nullKey=Null keys are not permitted
+
+futureToSendHandler.timeout=Operation timed out after waiting [{0}] [{1}] to complete
+
+perMessageDeflate.deflateFailed=Failed to decompress a compressed WebSocket frame
+perMessageDeflate.duplicateParameter=Duplicate definition of the [{0}] extension parameter
+perMessageDeflate.invalidWindowSize=An invalid windows of [{1}] size was specified for [{0}]. Valid values are whole numbers from 8 to 15 inclusive.
+perMessageDeflate.unknownParameter=An unknown extension parameter [{0}] was defined
+
+transformerFactory.unsupportedExtension=The extension [{0}] is not supported
+
+util.notToken=An illegal extension parameter was specified with name [{0}] and value [{1}]
+util.invalidMessageHandler=The message handler provided does not have an onMessage(Object) method
+util.invalidType=Unable to coerce value [{0}] to type [{1}]. That type is not supported.
+util.unknownDecoderType=The Decoder type [{0}] is not recognized
+
+# Note the wsFrame.* messages are used as close reasons in WebSocket control
+# frames and therefore must be 123 bytes (not characters) or less in length.
+# Messages are encoded using UTF-8 where a single character may be encoded in
+# as many as 4 bytes.
+wsFrame.alreadyResumed=Message receiving has already been resumed.
+wsFrame.alreadySuspended=Message receiving has already been suspended.
+wsFrame.bufferTooSmall=No async message support and buffer too small. Buffer size: [{0}], Message size: [{1}]
+wsFrame.byteToLongFail=Too many bytes ([{0}]) were provided to be converted into a long
+wsFrame.closed=New frame received after a close control frame
+wsFrame.controlFragmented=A fragmented control frame was received but control frames may not be fragmented
+wsFrame.controlPayloadTooBig=A control frame was sent with a payload of size [{0}] which is larger than the maximum permitted of 125 bytes
+wsFrame.controlNoFin=A control frame was sent that did not have the fin bit set. Control frames are not permitted to use continuation frames.
+wsFrame.illegalReadState=Unexpected read state [{0}]
+wsFrame.invalidOpCode= A WebSocket frame was sent with an unrecognised opCode of [{0}]
+wsFrame.invalidUtf8=A WebSocket text frame was received that could not be decoded to UTF-8 because it contained invalid byte sequences
+wsFrame.invalidUtf8Close=A WebSocket close frame was received with a close reason that contained invalid UTF-8 byte sequences
+wsFrame.ioeTriggeredClose=An unrecoverable IOException occurred so the connection was closed
+wsFrame.messageTooBig=The message was [{0}] bytes long but the MessageHandler has a limit of [{1}] bytes
+wsFrame.noContinuation=A new message was started when a continuation frame was expected
+wsFrame.notMasked=The client frame was not masked but all client frames must be masked
+wsFrame.oneByteCloseCode=The client sent a close frame with a single byte payload which is not valid
+wsFrame.partialHeaderComplete=WebSocket frame received. fin [{0}], rsv [{1}], OpCode [{2}], payload length [{3}]
+wsFrame.sessionClosed=The client data cannot be processed because the session has already been closed
+wsFrame.suspendRequested=Suspend of the message receiving has already been requested.
+wsFrame.textMessageTooBig=The decoded text message was too big for the output buffer and the endpoint does not support partial messages
+wsFrame.wrongRsv=The client frame set the reserved bits to [{0}] for a message with opCode [{1}] which was not supported by this endpoint
+
+wsFrameClient.ioe=Failure while reading data sent by server
+
+wsHandshakeRequest.invalidUri=The string [{0}] cannot be used to construct a valid URI
+wsHandshakeRequest.unknownScheme=The scheme [{0}] in the request is not recognised
+
+wsRemoteEndpoint.acquireTimeout=The current message was not fully sent within the specified timeout
+wsRemoteEndpoint.closed=Message will not be sent because the WebSocket session has been closed
+wsRemoteEndpoint.closedDuringMessage=The remainder of the message will not be sent because the WebSocket session has been closed
+wsRemoteEndpoint.closedOutputStream=This method may not be called as the OutputStream has been closed
+wsRemoteEndpoint.closedWriter=This method may not be called as the Writer has been closed
+wsRemoteEndpoint.changeType=When sending a fragmented message, all fragments must be of the same type
+wsRemoteEndpoint.concurrentMessageSend=Messages may not be sent concurrently even when using the asynchronous send messages. The client must wait for the previous message to complete before sending the next.
+wsRemoteEndpoint.flushOnCloseFailed=Batched messages still enabled after session has been closed. Unable to flush remaining batched message.
+wsRemoteEndpoint.invalidEncoder=The specified encoder of type [{0}] could not be instantiated
+wsRemoteEndpoint.noEncoder=No encoder specified for object of class [{0}]
+wsRemoteEndpoint.nullData=Invalid null data argument
+wsRemoteEndpoint.nullHandler=Invalid null handler argument
+wsRemoteEndpoint.sendInterrupt=The current thread was interrupted while waiting for a blocking send to complete
+wsRemoteEndpoint.tooMuchData=Ping or pong may not send more than 125 bytes
+wsRemoteEndpoint.wrongState=The remote endpoint was in state [{0}] which is an invalid state for called method
+
+# Note the following message is used as a close reason in a WebSocket control
+# frame and therefore must be 123 bytes (not characters) or less in length.
+# Messages are encoded using UTF-8 where a single character may be encoded in
+# as many as 4 bytes.
+wsSession.timeout=The WebSocket session [{0}] timeout expired
+
+wsSession.closed=The WebSocket session [{0}] has been closed and no method (apart from close()) may be called on a closed session
+wsSession.created=Created WebSocket session [{0}]
+wsSession.doClose=Closing WebSocket session [{1}]
+wsSession.duplicateHandlerBinary=A binary message handler has already been configured
+wsSession.duplicateHandlerPong=A pong message handler has already been configured
+wsSession.duplicateHandlerText=A text message handler has already been configured
+wsSession.invalidHandlerTypePong=A pong message handler must implement MessageHandler.Whole
+wsSession.flushFailOnClose=Failed to flush batched messages on session close
+wsSession.messageFailed=Unable to write the complete message as the WebSocket connection has been closed
+wsSession.sendCloseFail=Failed to send close message for session [{0}] to remote endpoint
+wsSession.removeHandlerFailed=Unable to remove the handler [{0}] as it was not registered with this session
+wsSession.unknownHandler=Unable to add the message handler [{0}] as it was for the unrecognised type [{1}]
+wsSession.unknownHandlerType=Unable to add the message handler [{0}] as it was wrapped as the unrecognised type [{1}]
+wsSession.instanceNew=Endpoint instance registration failed
+wsSession.instanceDestroy=Endpoint instance unregistration failed
+
+# Note the following message is used as a close reason in a WebSocket control
+# frame and therefore must be 123 bytes (not characters) or less in length.
+# Messages are encoded using UTF-8 where a single character may be encoded in
+# as many as 4 bytes.
+wsWebSocketContainer.shutdown=The web application is stopping
+
+wsWebSocketContainer.defaultConfiguratorFail=Failed to create the default configurator
+wsWebSocketContainer.endpointCreateFail=Failed to create a local endpoint of type [{0}]
+wsWebSocketContainer.maxBuffer=This implementation limits the maximum size of a buffer to Integer.MAX_VALUE
+wsWebSocketContainer.missingAnnotation=Cannot use POJO class [{0}] as it is not annotated with @ClientEndpoint
+wsWebSocketContainer.sessionCloseFail=Session with ID [{0}] did not close cleanly
+
+wsWebSocketContainer.asynchronousSocketChannelFail=Unable to open a connection to the server
+wsWebSocketContainer.httpRequestFailed=The HTTP request to initiate the WebSocket connection failed
+wsWebSocketContainer.invalidExtensionParameters=The server responded with extension parameters the client is unable to support
+wsWebSocketContainer.invalidHeader=Unable to parse HTTP header as no colon is present to delimit header name and header value in [{0}]. The header has been skipped.
+wsWebSocketContainer.invalidStatus=The HTTP response from the server [{0}] did not permit the HTTP upgrade to WebSocket
+wsWebSocketContainer.invalidSubProtocol=The WebSocket server returned multiple values for the Sec-WebSocket-Protocol header
+wsWebSocketContainer.pathNoHost=No host was specified in URI
+wsWebSocketContainer.pathWrongScheme=The scheme [{0}] is not supported. The supported schemes are ws and wss
+wsWebSocketContainer.proxyConnectFail=Failed to connect to the configured Proxy [{0}]. The HTTP response code was [{1}]
+wsWebSocketContainer.sslEngineFail=Unable to create SSLEngine to support SSL/TLS connections
+wsWebSocketContainer.missingLocationHeader=Failed to handle HTTP response code [{0}]. Missing Location header in response
+wsWebSocketContainer.redirectThreshold=Cyclic Location header [{0}] detected / reached max number of redirects [{1}] of max [{2}]
+wsWebSocketContainer.unsupportedAuthScheme=Failed to handle HTTP response code [{0}]. Unsupported Authentication scheme [{1}] returned in response
+wsWebSocketContainer.failedAuthentication=Failed to handle HTTP response code [{0}]. Authentication header was not accepted by server.
+wsWebSocketContainer.missingWWWAuthenticateHeader=Failed to handle HTTP response code [{0}]. Missing WWW-Authenticate header in response
diff --git a/src/java/nginx/unit/websocket/MessageHandlerResult.java b/src/java/nginx/unit/websocket/MessageHandlerResult.java
new file mode 100644
index 00000000..8d532d1e
--- /dev/null
+++ b/src/java/nginx/unit/websocket/MessageHandlerResult.java
@@ -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 nginx.unit.websocket;
+
+import javax.websocket.MessageHandler;
+
+public class MessageHandlerResult {
+
+ private final MessageHandler handler;
+ private final MessageHandlerResultType type;
+
+
+ public MessageHandlerResult(MessageHandler handler,
+ MessageHandlerResultType type) {
+ this.handler = handler;
+ this.type = type;
+ }
+
+
+ public MessageHandler getHandler() {
+ return handler;
+ }
+
+
+ public MessageHandlerResultType getType() {
+ return type;
+ }
+}
diff --git a/src/java/nginx/unit/websocket/MessageHandlerResultType.java b/src/java/nginx/unit/websocket/MessageHandlerResultType.java
new file mode 100644
index 00000000..1961bb4f
--- /dev/null
+++ b/src/java/nginx/unit/websocket/MessageHandlerResultType.java
@@ -0,0 +1,23 @@
+/*
+ * 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 nginx.unit.websocket;
+
+public enum MessageHandlerResultType {
+ BINARY,
+ TEXT,
+ PONG
+}
diff --git a/src/java/nginx/unit/websocket/MessagePart.java b/src/java/nginx/unit/websocket/MessagePart.java
new file mode 100644
index 00000000..b52c26f1
--- /dev/null
+++ b/src/java/nginx/unit/websocket/MessagePart.java
@@ -0,0 +1,83 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.SendHandler;
+
+class MessagePart {
+ private final boolean fin;
+ private final int rsv;
+ private final byte opCode;
+ private final ByteBuffer payload;
+ private final SendHandler intermediateHandler;
+ private volatile SendHandler endHandler;
+ private final long blockingWriteTimeoutExpiry;
+
+ public MessagePart( boolean fin, int rsv, byte opCode, ByteBuffer payload,
+ SendHandler intermediateHandler, SendHandler endHandler,
+ long blockingWriteTimeoutExpiry) {
+ this.fin = fin;
+ this.rsv = rsv;
+ this.opCode = opCode;
+ this.payload = payload;
+ this.intermediateHandler = intermediateHandler;
+ this.endHandler = endHandler;
+ this.blockingWriteTimeoutExpiry = blockingWriteTimeoutExpiry;
+ }
+
+
+ public boolean isFin() {
+ return fin;
+ }
+
+
+ public int getRsv() {
+ return rsv;
+ }
+
+
+ public byte getOpCode() {
+ return opCode;
+ }
+
+
+ public ByteBuffer getPayload() {
+ return payload;
+ }
+
+
+ public SendHandler getIntermediateHandler() {
+ return intermediateHandler;
+ }
+
+
+ public SendHandler getEndHandler() {
+ return endHandler;
+ }
+
+ public void setEndHandler(SendHandler endHandler) {
+ this.endHandler = endHandler;
+ }
+
+ public long getBlockingWriteTimeoutExpiry() {
+ return blockingWriteTimeoutExpiry;
+ }
+}
+
+
diff --git a/src/java/nginx/unit/websocket/PerMessageDeflate.java b/src/java/nginx/unit/websocket/PerMessageDeflate.java
new file mode 100644
index 00000000..88e0a0bc
--- /dev/null
+++ b/src/java/nginx/unit/websocket/PerMessageDeflate.java
@@ -0,0 +1,476 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.DataFormatException;
+import java.util.zip.Deflater;
+import java.util.zip.Inflater;
+
+import javax.websocket.Extension;
+import javax.websocket.Extension.Parameter;
+import javax.websocket.SendHandler;
+
+import org.apache.tomcat.util.res.StringManager;
+
+public class PerMessageDeflate implements Transformation {
+
+ private static final StringManager sm = StringManager.getManager(PerMessageDeflate.class);
+
+ private static final String SERVER_NO_CONTEXT_TAKEOVER = "server_no_context_takeover";
+ private static final String CLIENT_NO_CONTEXT_TAKEOVER = "client_no_context_takeover";
+ private static final String SERVER_MAX_WINDOW_BITS = "server_max_window_bits";
+ private static final String CLIENT_MAX_WINDOW_BITS = "client_max_window_bits";
+
+ private static final int RSV_BITMASK = 0b100;
+ private static final byte[] EOM_BYTES = new byte[] {0, 0, -1, -1};
+
+ public static final String NAME = "permessage-deflate";
+
+ private final boolean serverContextTakeover;
+ private final int serverMaxWindowBits;
+ private final boolean clientContextTakeover;
+ private final int clientMaxWindowBits;
+ private final boolean isServer;
+ private final Inflater inflater = new Inflater(true);
+ private final ByteBuffer readBuffer = ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE);
+ private final Deflater deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true);
+ private final byte[] EOM_BUFFER = new byte[EOM_BYTES.length + 1];
+
+ private volatile Transformation next;
+ private volatile boolean skipDecompression = false;
+ private volatile ByteBuffer writeBuffer = ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE);
+ private volatile boolean firstCompressedFrameWritten = false;
+ // Flag to track if a message is completely empty
+ private volatile boolean emptyMessage = true;
+
+ static PerMessageDeflate negotiate(List<List<Parameter>> preferences, boolean isServer) {
+ // Accept the first preference that the endpoint is able to support
+ for (List<Parameter> preference : preferences) {
+ boolean ok = true;
+ boolean serverContextTakeover = true;
+ int serverMaxWindowBits = -1;
+ boolean clientContextTakeover = true;
+ int clientMaxWindowBits = -1;
+
+ for (Parameter param : preference) {
+ if (SERVER_NO_CONTEXT_TAKEOVER.equals(param.getName())) {
+ if (serverContextTakeover) {
+ serverContextTakeover = false;
+ } else {
+ // Duplicate definition
+ throw new IllegalArgumentException(sm.getString(
+ "perMessageDeflate.duplicateParameter",
+ SERVER_NO_CONTEXT_TAKEOVER ));
+ }
+ } else if (CLIENT_NO_CONTEXT_TAKEOVER.equals(param.getName())) {
+ if (clientContextTakeover) {
+ clientContextTakeover = false;
+ } else {
+ // Duplicate definition
+ throw new IllegalArgumentException(sm.getString(
+ "perMessageDeflate.duplicateParameter",
+ CLIENT_NO_CONTEXT_TAKEOVER ));
+ }
+ } else if (SERVER_MAX_WINDOW_BITS.equals(param.getName())) {
+ if (serverMaxWindowBits == -1) {
+ serverMaxWindowBits = Integer.parseInt(param.getValue());
+ if (serverMaxWindowBits < 8 || serverMaxWindowBits > 15) {
+ throw new IllegalArgumentException(sm.getString(
+ "perMessageDeflate.invalidWindowSize",
+ SERVER_MAX_WINDOW_BITS,
+ Integer.valueOf(serverMaxWindowBits)));
+ }
+ // Java SE API (as of Java 8) does not expose the API to
+ // control the Window size. It is effectively hard-coded
+ // to 15
+ if (isServer && serverMaxWindowBits != 15) {
+ ok = false;
+ break;
+ // Note server window size is not an issue for the
+ // client since the client will assume 15 and if the
+ // server uses a smaller window everything will
+ // still work
+ }
+ } else {
+ // Duplicate definition
+ throw new IllegalArgumentException(sm.getString(
+ "perMessageDeflate.duplicateParameter",
+ SERVER_MAX_WINDOW_BITS ));
+ }
+ } else if (CLIENT_MAX_WINDOW_BITS.equals(param.getName())) {
+ if (clientMaxWindowBits == -1) {
+ if (param.getValue() == null) {
+ // Hint to server that the client supports this
+ // option. Java SE API (as of Java 8) does not
+ // expose the API to control the Window size. It is
+ // effectively hard-coded to 15
+ clientMaxWindowBits = 15;
+ } else {
+ clientMaxWindowBits = Integer.parseInt(param.getValue());
+ if (clientMaxWindowBits < 8 || clientMaxWindowBits > 15) {
+ throw new IllegalArgumentException(sm.getString(
+ "perMessageDeflate.invalidWindowSize",
+ CLIENT_MAX_WINDOW_BITS,
+ Integer.valueOf(clientMaxWindowBits)));
+ }
+ }
+ // Java SE API (as of Java 8) does not expose the API to
+ // control the Window size. It is effectively hard-coded
+ // to 15
+ if (!isServer && clientMaxWindowBits != 15) {
+ ok = false;
+ break;
+ // Note client window size is not an issue for the
+ // server since the server will assume 15 and if the
+ // client uses a smaller window everything will
+ // still work
+ }
+ } else {
+ // Duplicate definition
+ throw new IllegalArgumentException(sm.getString(
+ "perMessageDeflate.duplicateParameter",
+ CLIENT_MAX_WINDOW_BITS ));
+ }
+ } else {
+ // Unknown parameter
+ throw new IllegalArgumentException(sm.getString(
+ "perMessageDeflate.unknownParameter", param.getName()));
+ }
+ }
+ if (ok) {
+ return new PerMessageDeflate(serverContextTakeover, serverMaxWindowBits,
+ clientContextTakeover, clientMaxWindowBits, isServer);
+ }
+ }
+ // Failed to negotiate agreeable terms
+ return null;
+ }
+
+
+ private PerMessageDeflate(boolean serverContextTakeover, int serverMaxWindowBits,
+ boolean clientContextTakeover, int clientMaxWindowBits, boolean isServer) {
+ this.serverContextTakeover = serverContextTakeover;
+ this.serverMaxWindowBits = serverMaxWindowBits;
+ this.clientContextTakeover = clientContextTakeover;
+ this.clientMaxWindowBits = clientMaxWindowBits;
+ this.isServer = isServer;
+ }
+
+
+ @Override
+ public TransformationResult getMoreData(byte opCode, boolean fin, int rsv, ByteBuffer dest)
+ throws IOException {
+ // Control frames are never compressed and may appear in the middle of
+ // a WebSocket method. Pass them straight through.
+ if (Util.isControl(opCode)) {
+ return next.getMoreData(opCode, fin, rsv, dest);
+ }
+
+ if (!Util.isContinuation(opCode)) {
+ // First frame in new message
+ skipDecompression = (rsv & RSV_BITMASK) == 0;
+ }
+
+ // Pass uncompressed frames straight through.
+ if (skipDecompression) {
+ return next.getMoreData(opCode, fin, rsv, dest);
+ }
+
+ int written;
+ boolean usedEomBytes = false;
+
+ while (dest.remaining() > 0) {
+ // Space available in destination. Try and fill it.
+ try {
+ written = inflater.inflate(
+ dest.array(), dest.arrayOffset() + dest.position(), dest.remaining());
+ } catch (DataFormatException e) {
+ throw new IOException(sm.getString("perMessageDeflate.deflateFailed"), e);
+ }
+ dest.position(dest.position() + written);
+
+ if (inflater.needsInput() && !usedEomBytes ) {
+ if (dest.hasRemaining()) {
+ readBuffer.clear();
+ TransformationResult nextResult =
+ next.getMoreData(opCode, fin, (rsv ^ RSV_BITMASK), readBuffer);
+ inflater.setInput(
+ readBuffer.array(), readBuffer.arrayOffset(), readBuffer.position());
+ if (TransformationResult.UNDERFLOW.equals(nextResult)) {
+ return nextResult;
+ } else if (TransformationResult.END_OF_FRAME.equals(nextResult) &&
+ readBuffer.position() == 0) {
+ if (fin) {
+ inflater.setInput(EOM_BYTES);
+ usedEomBytes = true;
+ } else {
+ return TransformationResult.END_OF_FRAME;
+ }
+ }
+ }
+ } else if (written == 0) {
+ if (fin && (isServer && !clientContextTakeover ||
+ !isServer && !serverContextTakeover)) {
+ inflater.reset();
+ }
+ return TransformationResult.END_OF_FRAME;
+ }
+ }
+
+ return TransformationResult.OVERFLOW;
+ }
+
+
+ @Override
+ public boolean validateRsv(int rsv, byte opCode) {
+ if (Util.isControl(opCode)) {
+ if ((rsv & RSV_BITMASK) != 0) {
+ return false;
+ } else {
+ if (next == null) {
+ return true;
+ } else {
+ return next.validateRsv(rsv, opCode);
+ }
+ }
+ } else {
+ int rsvNext = rsv;
+ if ((rsv & RSV_BITMASK) != 0) {
+ rsvNext = rsv ^ RSV_BITMASK;
+ }
+ if (next == null) {
+ return true;
+ } else {
+ return next.validateRsv(rsvNext, opCode);
+ }
+ }
+ }
+
+
+ @Override
+ public Extension getExtensionResponse() {
+ Extension result = new WsExtension(NAME);
+
+ List<Extension.Parameter> params = result.getParameters();
+
+ if (!serverContextTakeover) {
+ params.add(new WsExtensionParameter(SERVER_NO_CONTEXT_TAKEOVER, null));
+ }
+ if (serverMaxWindowBits != -1) {
+ params.add(new WsExtensionParameter(SERVER_MAX_WINDOW_BITS,
+ Integer.toString(serverMaxWindowBits)));
+ }
+ if (!clientContextTakeover) {
+ params.add(new WsExtensionParameter(CLIENT_NO_CONTEXT_TAKEOVER, null));
+ }
+ if (clientMaxWindowBits != -1) {
+ params.add(new WsExtensionParameter(CLIENT_MAX_WINDOW_BITS,
+ Integer.toString(clientMaxWindowBits)));
+ }
+
+ return result;
+ }
+
+
+ @Override
+ public void setNext(Transformation t) {
+ if (next == null) {
+ this.next = t;
+ } else {
+ next.setNext(t);
+ }
+ }
+
+
+ @Override
+ public boolean validateRsvBits(int i) {
+ if ((i & RSV_BITMASK) != 0) {
+ return false;
+ }
+ if (next == null) {
+ return true;
+ } else {
+ return next.validateRsvBits(i | RSV_BITMASK);
+ }
+ }
+
+
+ @Override
+ public List<MessagePart> sendMessagePart(List<MessagePart> uncompressedParts) {
+ List<MessagePart> allCompressedParts = new ArrayList<>();
+
+ for (MessagePart uncompressedPart : uncompressedParts) {
+ byte opCode = uncompressedPart.getOpCode();
+ boolean emptyPart = uncompressedPart.getPayload().limit() == 0;
+ emptyMessage = emptyMessage && emptyPart;
+ if (Util.isControl(opCode)) {
+ // Control messages can appear in the middle of other messages
+ // and must not be compressed. Pass it straight through
+ allCompressedParts.add(uncompressedPart);
+ } else if (emptyMessage && uncompressedPart.isFin()) {
+ // Zero length messages can't be compressed so pass the
+ // final (empty) part straight through.
+ allCompressedParts.add(uncompressedPart);
+ } else {
+ List<MessagePart> compressedParts = new ArrayList<>();
+ ByteBuffer uncompressedPayload = uncompressedPart.getPayload();
+ SendHandler uncompressedIntermediateHandler =
+ uncompressedPart.getIntermediateHandler();
+
+ deflater.setInput(uncompressedPayload.array(),
+ uncompressedPayload.arrayOffset() + uncompressedPayload.position(),
+ uncompressedPayload.remaining());
+
+ int flush = (uncompressedPart.isFin() ? Deflater.SYNC_FLUSH : Deflater.NO_FLUSH);
+ boolean deflateRequired = true;
+
+ while (deflateRequired) {
+ ByteBuffer compressedPayload = writeBuffer;
+
+ int written = deflater.deflate(compressedPayload.array(),
+ compressedPayload.arrayOffset() + compressedPayload.position(),
+ compressedPayload.remaining(), flush);
+ compressedPayload.position(compressedPayload.position() + written);
+
+ if (!uncompressedPart.isFin() && compressedPayload.hasRemaining() && deflater.needsInput()) {
+ // This message part has been fully processed by the
+ // deflater. Fire the send handler for this message part
+ // and move on to the next message part.
+ break;
+ }
+
+ // If this point is reached, a new compressed message part
+ // will be created...
+ MessagePart compressedPart;
+
+ // .. and a new writeBuffer will be required.
+ writeBuffer = ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE);
+
+ // Flip the compressed payload ready for writing
+ compressedPayload.flip();
+
+ boolean fin = uncompressedPart.isFin();
+ boolean full = compressedPayload.limit() == compressedPayload.capacity();
+ boolean needsInput = deflater.needsInput();
+ long blockingWriteTimeoutExpiry = uncompressedPart.getBlockingWriteTimeoutExpiry();
+
+ if (fin && !full && needsInput) {
+ // End of compressed message. Drop EOM bytes and output.
+ compressedPayload.limit(compressedPayload.limit() - EOM_BYTES.length);
+ compressedPart = new MessagePart(true, getRsv(uncompressedPart),
+ opCode, compressedPayload, uncompressedIntermediateHandler,
+ uncompressedIntermediateHandler, blockingWriteTimeoutExpiry);
+ deflateRequired = false;
+ startNewMessage();
+ } else if (full && !needsInput) {
+ // Write buffer full and input message not fully read.
+ // Output and start new compressed part.
+ compressedPart = new MessagePart(false, getRsv(uncompressedPart),
+ opCode, compressedPayload, uncompressedIntermediateHandler,
+ uncompressedIntermediateHandler, blockingWriteTimeoutExpiry);
+ } else if (!fin && full && needsInput) {
+ // Write buffer full and input message not fully read.
+ // Output and get more data.
+ compressedPart = new MessagePart(false, getRsv(uncompressedPart),
+ opCode, compressedPayload, uncompressedIntermediateHandler,
+ uncompressedIntermediateHandler, blockingWriteTimeoutExpiry);
+ deflateRequired = false;
+ } else if (fin && full && needsInput) {
+ // Write buffer full. Input fully read. Deflater may be
+ // in one of four states:
+ // - output complete (just happened to align with end of
+ // buffer
+ // - in middle of EOM bytes
+ // - about to write EOM bytes
+ // - more data to write
+ int eomBufferWritten = deflater.deflate(EOM_BUFFER, 0, EOM_BUFFER.length, Deflater.SYNC_FLUSH);
+ if (eomBufferWritten < EOM_BUFFER.length) {
+ // EOM has just been completed
+ compressedPayload.limit(compressedPayload.limit() - EOM_BYTES.length + eomBufferWritten);
+ compressedPart = new MessagePart(true,
+ getRsv(uncompressedPart), opCode, compressedPayload,
+ uncompressedIntermediateHandler, uncompressedIntermediateHandler,
+ blockingWriteTimeoutExpiry);
+ deflateRequired = false;
+ startNewMessage();
+ } else {
+ // More data to write
+ // Copy bytes to new write buffer
+ writeBuffer.put(EOM_BUFFER, 0, eomBufferWritten);
+ compressedPart = new MessagePart(false,
+ getRsv(uncompressedPart), opCode, compressedPayload,
+ uncompressedIntermediateHandler, uncompressedIntermediateHandler,
+ blockingWriteTimeoutExpiry);
+ }
+ } else {
+ throw new IllegalStateException("Should never happen");
+ }
+
+ // Add the newly created compressed part to the set of parts
+ // to pass on to the next transformation.
+ compressedParts.add(compressedPart);
+ }
+
+ SendHandler uncompressedEndHandler = uncompressedPart.getEndHandler();
+ int size = compressedParts.size();
+ if (size > 0) {
+ compressedParts.get(size - 1).setEndHandler(uncompressedEndHandler);
+ }
+
+ allCompressedParts.addAll(compressedParts);
+ }
+ }
+
+ if (next == null) {
+ return allCompressedParts;
+ } else {
+ return next.sendMessagePart(allCompressedParts);
+ }
+ }
+
+
+ private void startNewMessage() {
+ firstCompressedFrameWritten = false;
+ emptyMessage = true;
+ if (isServer && !serverContextTakeover || !isServer && !clientContextTakeover) {
+ deflater.reset();
+ }
+ }
+
+
+ private int getRsv(MessagePart uncompressedMessagePart) {
+ int result = uncompressedMessagePart.getRsv();
+ if (!firstCompressedFrameWritten) {
+ result += RSV_BITMASK;
+ firstCompressedFrameWritten = true;
+ }
+ return result;
+ }
+
+
+ @Override
+ public void close() {
+ // There will always be a next transformation
+ next.close();
+ inflater.end();
+ deflater.end();
+ }
+}
diff --git a/src/java/nginx/unit/websocket/ReadBufferOverflowException.java b/src/java/nginx/unit/websocket/ReadBufferOverflowException.java
new file mode 100644
index 00000000..9ce7ac27
--- /dev/null
+++ b/src/java/nginx/unit/websocket/ReadBufferOverflowException.java
@@ -0,0 +1,34 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.io.IOException;
+
+public class ReadBufferOverflowException extends IOException {
+
+ private static final long serialVersionUID = 1L;
+
+ private final int minBufferSize;
+
+ public ReadBufferOverflowException(int minBufferSize) {
+ this.minBufferSize = minBufferSize;
+ }
+
+ public int getMinBufferSize() {
+ return minBufferSize;
+ }
+}
diff --git a/src/java/nginx/unit/websocket/Transformation.java b/src/java/nginx/unit/websocket/Transformation.java
new file mode 100644
index 00000000..45474c7d
--- /dev/null
+++ b/src/java/nginx/unit/websocket/Transformation.java
@@ -0,0 +1,111 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import javax.websocket.Extension;
+
+/**
+ * The internal representation of the transformation that a WebSocket extension
+ * performs on a message.
+ */
+public interface Transformation {
+
+ /**
+ * Sets the next transformation in the pipeline.
+ * @param t The next transformation
+ */
+ void setNext(Transformation t);
+
+ /**
+ * Validate that the RSV bit(s) required by this transformation are not
+ * being used by another extension. The implementation is expected to set
+ * any bits it requires before passing the set of in-use bits to the next
+ * transformation.
+ *
+ * @param i The RSV bits marked as in use so far as an int in the
+ * range zero to seven with RSV1 as the MSB and RSV3 as the
+ * LSB
+ *
+ * @return <code>true</code> if the combination of RSV bits used by the
+ * transformations in the pipeline do not conflict otherwise
+ * <code>false</code>
+ */
+ boolean validateRsvBits(int i);
+
+ /**
+ * Obtain the extension that describes the information to be returned to the
+ * client.
+ *
+ * @return The extension information that describes the parameters that have
+ * been agreed for this transformation
+ */
+ Extension getExtensionResponse();
+
+ /**
+ * Obtain more input data.
+ *
+ * @param opCode The opcode for the frame currently being processed
+ * @param fin Is this the final frame in this WebSocket message?
+ * @param rsv The reserved bits for the frame currently being
+ * processed
+ * @param dest The buffer in which the data is to be written
+ *
+ * @return The result of trying to read more data from the transform
+ *
+ * @throws IOException If an I/O error occurs while reading data from the
+ * transform
+ */
+ TransformationResult getMoreData(byte opCode, boolean fin, int rsv, ByteBuffer dest) throws IOException;
+
+ /**
+ * Validates the RSV and opcode combination (assumed to have been extracted
+ * from a WebSocket Frame) for this extension. The implementation is
+ * expected to unset any RSV bits it has validated before passing the
+ * remaining RSV bits to the next transformation in the pipeline.
+ *
+ * @param rsv The RSV bits received as an int in the range zero to
+ * seven with RSV1 as the MSB and RSV3 as the LSB
+ * @param opCode The opCode received
+ *
+ * @return <code>true</code> if the RSV is valid otherwise
+ * <code>false</code>
+ */
+ boolean validateRsv(int rsv, byte opCode);
+
+ /**
+ * Takes the provided list of messages, transforms them, passes the
+ * transformed list on to the next transformation (if any) and then returns
+ * the resulting list of message parts after all of the transformations have
+ * been applied.
+ *
+ * @param messageParts The list of messages to be transformed
+ *
+ * @return The list of messages after this any any subsequent
+ * transformations have been applied. The size of the returned list
+ * may be bigger or smaller than the size of the input list
+ */
+ List<MessagePart> sendMessagePart(List<MessagePart> messageParts);
+
+ /**
+ * Clean-up any resources that were used by the transformation.
+ */
+ void close();
+}
diff --git a/src/java/nginx/unit/websocket/TransformationFactory.java b/src/java/nginx/unit/websocket/TransformationFactory.java
new file mode 100644
index 00000000..fac04555
--- /dev/null
+++ b/src/java/nginx/unit/websocket/TransformationFactory.java
@@ -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 nginx.unit.websocket;
+
+import java.util.List;
+
+import javax.websocket.Extension;
+
+import org.apache.tomcat.util.res.StringManager;
+
+public class TransformationFactory {
+
+ private static final StringManager sm = StringManager.getManager(TransformationFactory.class);
+
+ private static final TransformationFactory factory = new TransformationFactory();
+
+ private TransformationFactory() {
+ // Hide default constructor
+ }
+
+ public static TransformationFactory getInstance() {
+ return factory;
+ }
+
+ public Transformation create(String name, List<List<Extension.Parameter>> preferences,
+ boolean isServer) {
+ if (PerMessageDeflate.NAME.equals(name)) {
+ return PerMessageDeflate.negotiate(preferences, isServer);
+ }
+ if (Constants.ALLOW_UNSUPPORTED_EXTENSIONS) {
+ return null;
+ } else {
+ throw new IllegalArgumentException(
+ sm.getString("transformerFactory.unsupportedExtension", name));
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/TransformationResult.java b/src/java/nginx/unit/websocket/TransformationResult.java
new file mode 100644
index 00000000..0de35e55
--- /dev/null
+++ b/src/java/nginx/unit/websocket/TransformationResult.java
@@ -0,0 +1,37 @@
+/*
+ * 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 nginx.unit.websocket;
+
+public enum TransformationResult {
+ /**
+ * The end of the available data was reached before the WebSocket frame was
+ * completely read.
+ */
+ UNDERFLOW,
+
+ /**
+ * The provided destination buffer was filled before all of the available
+ * data from the WebSocket frame could be processed.
+ */
+ OVERFLOW,
+
+ /**
+ * The end of the WebSocket frame was reached and all the data from that
+ * frame processed into the provided destination buffer.
+ */
+ END_OF_FRAME
+}
diff --git a/src/java/nginx/unit/websocket/Util.java b/src/java/nginx/unit/websocket/Util.java
new file mode 100644
index 00000000..6acf3ade
--- /dev/null
+++ b/src/java/nginx/unit/websocket/Util.java
@@ -0,0 +1,666 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import javax.websocket.CloseReason.CloseCode;
+import javax.websocket.CloseReason.CloseCodes;
+import javax.websocket.Decoder;
+import javax.websocket.Decoder.Binary;
+import javax.websocket.Decoder.BinaryStream;
+import javax.websocket.Decoder.Text;
+import javax.websocket.Decoder.TextStream;
+import javax.websocket.DeploymentException;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+import javax.websocket.Extension;
+import javax.websocket.MessageHandler;
+import javax.websocket.PongMessage;
+import javax.websocket.Session;
+
+import org.apache.tomcat.util.res.StringManager;
+import nginx.unit.websocket.pojo.PojoMessageHandlerPartialBinary;
+import nginx.unit.websocket.pojo.PojoMessageHandlerWholeBinary;
+import nginx.unit.websocket.pojo.PojoMessageHandlerWholeText;
+
+/**
+ * Utility class for internal use only within the
+ * {@link nginx.unit.websocket} package.
+ */
+public class Util {
+
+ private static final StringManager sm = StringManager.getManager(Util.class);
+ private static final Queue<SecureRandom> randoms =
+ new ConcurrentLinkedQueue<>();
+
+ private Util() {
+ // Hide default constructor
+ }
+
+
+ static boolean isControl(byte opCode) {
+ return (opCode & 0x08) != 0;
+ }
+
+
+ static boolean isText(byte opCode) {
+ return opCode == Constants.OPCODE_TEXT;
+ }
+
+
+ static boolean isContinuation(byte opCode) {
+ return opCode == Constants.OPCODE_CONTINUATION;
+ }
+
+
+ static CloseCode getCloseCode(int code) {
+ if (code > 2999 && code < 5000) {
+ return CloseCodes.getCloseCode(code);
+ }
+ switch (code) {
+ case 1000:
+ return CloseCodes.NORMAL_CLOSURE;
+ case 1001:
+ return CloseCodes.GOING_AWAY;
+ case 1002:
+ return CloseCodes.PROTOCOL_ERROR;
+ case 1003:
+ return CloseCodes.CANNOT_ACCEPT;
+ case 1004:
+ // Should not be used in a close frame
+ // return CloseCodes.RESERVED;
+ return CloseCodes.PROTOCOL_ERROR;
+ case 1005:
+ // Should not be used in a close frame
+ // return CloseCodes.NO_STATUS_CODE;
+ return CloseCodes.PROTOCOL_ERROR;
+ case 1006:
+ // Should not be used in a close frame
+ // return CloseCodes.CLOSED_ABNORMALLY;
+ return CloseCodes.PROTOCOL_ERROR;
+ case 1007:
+ return CloseCodes.NOT_CONSISTENT;
+ case 1008:
+ return CloseCodes.VIOLATED_POLICY;
+ case 1009:
+ return CloseCodes.TOO_BIG;
+ case 1010:
+ return CloseCodes.NO_EXTENSION;
+ case 1011:
+ return CloseCodes.UNEXPECTED_CONDITION;
+ case 1012:
+ // Not in RFC6455
+ // return CloseCodes.SERVICE_RESTART;
+ return CloseCodes.PROTOCOL_ERROR;
+ case 1013:
+ // Not in RFC6455
+ // return CloseCodes.TRY_AGAIN_LATER;
+ return CloseCodes.PROTOCOL_ERROR;
+ case 1015:
+ // Should not be used in a close frame
+ // return CloseCodes.TLS_HANDSHAKE_FAILURE;
+ return CloseCodes.PROTOCOL_ERROR;
+ default:
+ return CloseCodes.PROTOCOL_ERROR;
+ }
+ }
+
+
+ static byte[] generateMask() {
+ // SecureRandom is not thread-safe so need to make sure only one thread
+ // uses it at a time. In theory, the pool could grow to the same size
+ // as the number of request processing threads. In reality it will be
+ // a lot smaller.
+
+ // Get a SecureRandom from the pool
+ SecureRandom sr = randoms.poll();
+
+ // If one isn't available, generate a new one
+ if (sr == null) {
+ try {
+ sr = SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ // Fall back to platform default
+ sr = new SecureRandom();
+ }
+ }
+
+ // Generate the mask
+ byte[] result = new byte[4];
+ sr.nextBytes(result);
+
+ // Put the SecureRandom back in the poll
+ randoms.add(sr);
+
+ return result;
+ }
+
+
+ static Class<?> getMessageType(MessageHandler listener) {
+ return Util.getGenericType(MessageHandler.class,
+ listener.getClass()).getClazz();
+ }
+
+
+ private static Class<?> getDecoderType(Class<? extends Decoder> decoder) {
+ return Util.getGenericType(Decoder.class, decoder).getClazz();
+ }
+
+
+ static Class<?> getEncoderType(Class<? extends Encoder> encoder) {
+ return Util.getGenericType(Encoder.class, encoder).getClazz();
+ }
+
+
+ private static <T> TypeResult getGenericType(Class<T> type,
+ Class<? extends T> clazz) {
+
+ // Look to see if this class implements the interface of interest
+
+ // Get all the interfaces
+ Type[] interfaces = clazz.getGenericInterfaces();
+ for (Type iface : interfaces) {
+ // Only need to check interfaces that use generics
+ if (iface instanceof ParameterizedType) {
+ ParameterizedType pi = (ParameterizedType) iface;
+ // Look for the interface of interest
+ if (pi.getRawType() instanceof Class) {
+ if (type.isAssignableFrom((Class<?>) pi.getRawType())) {
+ return getTypeParameter(
+ clazz, pi.getActualTypeArguments()[0]);
+ }
+ }
+ }
+ }
+
+ // Interface not found on this class. Look at the superclass.
+ @SuppressWarnings("unchecked")
+ Class<? extends T> superClazz =
+ (Class<? extends T>) clazz.getSuperclass();
+ if (superClazz == null) {
+ // Finished looking up the class hierarchy without finding anything
+ return null;
+ }
+
+ TypeResult superClassTypeResult = getGenericType(type, superClazz);
+ int dimension = superClassTypeResult.getDimension();
+ if (superClassTypeResult.getIndex() == -1 && dimension == 0) {
+ // Superclass implements interface and defines explicit type for
+ // the interface of interest
+ return superClassTypeResult;
+ }
+
+ if (superClassTypeResult.getIndex() > -1) {
+ // Superclass implements interface and defines unknown type for
+ // the interface of interest
+ // Map that unknown type to the generic types defined in this class
+ ParameterizedType superClassType =
+ (ParameterizedType) clazz.getGenericSuperclass();
+ TypeResult result = getTypeParameter(clazz,
+ superClassType.getActualTypeArguments()[
+ superClassTypeResult.getIndex()]);
+ result.incrementDimension(superClassTypeResult.getDimension());
+ if (result.getClazz() != null && result.getDimension() > 0) {
+ superClassTypeResult = result;
+ } else {
+ return result;
+ }
+ }
+
+ if (superClassTypeResult.getDimension() > 0) {
+ StringBuilder className = new StringBuilder();
+ for (int i = 0; i < dimension; i++) {
+ className.append('[');
+ }
+ className.append('L');
+ className.append(superClassTypeResult.getClazz().getCanonicalName());
+ className.append(';');
+
+ Class<?> arrayClazz;
+ try {
+ arrayClazz = Class.forName(className.toString());
+ } catch (ClassNotFoundException e) {
+ throw new IllegalArgumentException(e);
+ }
+
+ return new TypeResult(arrayClazz, -1, 0);
+ }
+
+ // Error will be logged further up the call stack
+ return null;
+ }
+
+
+ /*
+ * For a generic parameter, return either the Class used or if the type
+ * is unknown, the index for the type in definition of the class
+ */
+ private static TypeResult getTypeParameter(Class<?> clazz, Type argType) {
+ if (argType instanceof Class<?>) {
+ return new TypeResult((Class<?>) argType, -1, 0);
+ } else if (argType instanceof ParameterizedType) {
+ return new TypeResult((Class<?>)((ParameterizedType) argType).getRawType(), -1, 0);
+ } else if (argType instanceof GenericArrayType) {
+ Type arrayElementType = ((GenericArrayType) argType).getGenericComponentType();
+ TypeResult result = getTypeParameter(clazz, arrayElementType);
+ result.incrementDimension(1);
+ return result;
+ } else {
+ TypeVariable<?>[] tvs = clazz.getTypeParameters();
+ for (int i = 0; i < tvs.length; i++) {
+ if (tvs[i].equals(argType)) {
+ return new TypeResult(null, i, 0);
+ }
+ }
+ return null;
+ }
+ }
+
+
+ public static boolean isPrimitive(Class<?> clazz) {
+ if (clazz.isPrimitive()) {
+ return true;
+ } else if(clazz.equals(Boolean.class) ||
+ clazz.equals(Byte.class) ||
+ clazz.equals(Character.class) ||
+ clazz.equals(Double.class) ||
+ clazz.equals(Float.class) ||
+ clazz.equals(Integer.class) ||
+ clazz.equals(Long.class) ||
+ clazz.equals(Short.class)) {
+ return true;
+ }
+ return false;
+ }
+
+
+ public static Object coerceToType(Class<?> type, String value) {
+ if (type.equals(String.class)) {
+ return value;
+ } else if (type.equals(boolean.class) || type.equals(Boolean.class)) {
+ return Boolean.valueOf(value);
+ } else if (type.equals(byte.class) || type.equals(Byte.class)) {
+ return Byte.valueOf(value);
+ } else if (type.equals(char.class) || type.equals(Character.class)) {
+ return Character.valueOf(value.charAt(0));
+ } else if (type.equals(double.class) || type.equals(Double.class)) {
+ return Double.valueOf(value);
+ } else if (type.equals(float.class) || type.equals(Float.class)) {
+ return Float.valueOf(value);
+ } else if (type.equals(int.class) || type.equals(Integer.class)) {
+ return Integer.valueOf(value);
+ } else if (type.equals(long.class) || type.equals(Long.class)) {
+ return Long.valueOf(value);
+ } else if (type.equals(short.class) || type.equals(Short.class)) {
+ return Short.valueOf(value);
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "util.invalidType", value, type.getName()));
+ }
+ }
+
+
+ public static List<DecoderEntry> getDecoders(
+ List<Class<? extends Decoder>> decoderClazzes)
+ throws DeploymentException{
+
+ List<DecoderEntry> result = new ArrayList<>();
+ if (decoderClazzes != null) {
+ for (Class<? extends Decoder> decoderClazz : decoderClazzes) {
+ // Need to instantiate decoder to ensure it is valid and that
+ // deployment can be failed if it is not
+ @SuppressWarnings("unused")
+ Decoder instance;
+ try {
+ instance = decoderClazz.getConstructor().newInstance();
+ } catch (ReflectiveOperationException e) {
+ throw new DeploymentException(
+ sm.getString("pojoMethodMapping.invalidDecoder",
+ decoderClazz.getName()), e);
+ }
+ DecoderEntry entry = new DecoderEntry(
+ Util.getDecoderType(decoderClazz), decoderClazz);
+ result.add(entry);
+ }
+ }
+
+ return result;
+ }
+
+
+ static Set<MessageHandlerResult> getMessageHandlers(Class<?> target,
+ MessageHandler listener, EndpointConfig endpointConfig,
+ Session session) {
+
+ // Will never be more than 2 types
+ Set<MessageHandlerResult> results = new HashSet<>(2);
+
+ // Simple cases - handlers already accepts one of the types expected by
+ // the frame handling code
+ if (String.class.isAssignableFrom(target)) {
+ MessageHandlerResult result =
+ new MessageHandlerResult(listener,
+ MessageHandlerResultType.TEXT);
+ results.add(result);
+ } else if (ByteBuffer.class.isAssignableFrom(target)) {
+ MessageHandlerResult result =
+ new MessageHandlerResult(listener,
+ MessageHandlerResultType.BINARY);
+ results.add(result);
+ } else if (PongMessage.class.isAssignableFrom(target)) {
+ MessageHandlerResult result =
+ new MessageHandlerResult(listener,
+ MessageHandlerResultType.PONG);
+ results.add(result);
+ // Handler needs wrapping and optional decoder to convert it to one of
+ // the types expected by the frame handling code
+ } else if (byte[].class.isAssignableFrom(target)) {
+ boolean whole = MessageHandler.Whole.class.isAssignableFrom(listener.getClass());
+ MessageHandlerResult result = new MessageHandlerResult(
+ whole ? new PojoMessageHandlerWholeBinary(listener,
+ getOnMessageMethod(listener), session,
+ endpointConfig, matchDecoders(target, endpointConfig, true),
+ new Object[1], 0, true, -1, false, -1) :
+ new PojoMessageHandlerPartialBinary(listener,
+ getOnMessagePartialMethod(listener), session,
+ new Object[2], 0, true, 1, -1, -1),
+ MessageHandlerResultType.BINARY);
+ results.add(result);
+ } else if (InputStream.class.isAssignableFrom(target)) {
+ MessageHandlerResult result = new MessageHandlerResult(
+ new PojoMessageHandlerWholeBinary(listener,
+ getOnMessageMethod(listener), session,
+ endpointConfig, matchDecoders(target, endpointConfig, true),
+ new Object[1], 0, true, -1, true, -1),
+ MessageHandlerResultType.BINARY);
+ results.add(result);
+ } else if (Reader.class.isAssignableFrom(target)) {
+ MessageHandlerResult result = new MessageHandlerResult(
+ new PojoMessageHandlerWholeText(listener,
+ getOnMessageMethod(listener), session,
+ endpointConfig, matchDecoders(target, endpointConfig, false),
+ new Object[1], 0, true, -1, -1),
+ MessageHandlerResultType.TEXT);
+ results.add(result);
+ } else {
+ // Handler needs wrapping and requires decoder to convert it to one
+ // of the types expected by the frame handling code
+ DecoderMatch decoderMatch = matchDecoders(target, endpointConfig);
+ Method m = getOnMessageMethod(listener);
+ if (decoderMatch.getBinaryDecoders().size() > 0) {
+ MessageHandlerResult result = new MessageHandlerResult(
+ new PojoMessageHandlerWholeBinary(listener, m, session,
+ endpointConfig,
+ decoderMatch.getBinaryDecoders(), new Object[1],
+ 0, false, -1, false, -1),
+ MessageHandlerResultType.BINARY);
+ results.add(result);
+ }
+ if (decoderMatch.getTextDecoders().size() > 0) {
+ MessageHandlerResult result = new MessageHandlerResult(
+ new PojoMessageHandlerWholeText(listener, m, session,
+ endpointConfig,
+ decoderMatch.getTextDecoders(), new Object[1],
+ 0, false, -1, -1),
+ MessageHandlerResultType.TEXT);
+ results.add(result);
+ }
+ }
+
+ if (results.size() == 0) {
+ throw new IllegalArgumentException(
+ sm.getString("wsSession.unknownHandler", listener, target));
+ }
+
+ return results;
+ }
+
+ private static List<Class<? extends Decoder>> matchDecoders(Class<?> target,
+ EndpointConfig endpointConfig, boolean binary) {
+ DecoderMatch decoderMatch = matchDecoders(target, endpointConfig);
+ if (binary) {
+ if (decoderMatch.getBinaryDecoders().size() > 0) {
+ return decoderMatch.getBinaryDecoders();
+ }
+ } else if (decoderMatch.getTextDecoders().size() > 0) {
+ return decoderMatch.getTextDecoders();
+ }
+ return null;
+ }
+
+ private static DecoderMatch matchDecoders(Class<?> target,
+ EndpointConfig endpointConfig) {
+ DecoderMatch decoderMatch;
+ try {
+ List<Class<? extends Decoder>> decoders =
+ endpointConfig.getDecoders();
+ List<DecoderEntry> decoderEntries = getDecoders(decoders);
+ decoderMatch = new DecoderMatch(target, decoderEntries);
+ } catch (DeploymentException e) {
+ throw new IllegalArgumentException(e);
+ }
+ return decoderMatch;
+ }
+
+ public static void parseExtensionHeader(List<Extension> extensions,
+ String header) {
+ // The relevant ABNF for the Sec-WebSocket-Extensions is as follows:
+ // extension-list = 1#extension
+ // extension = extension-token *( ";" extension-param )
+ // extension-token = registered-token
+ // registered-token = token
+ // extension-param = token [ "=" (token | quoted-string) ]
+ // ; When using the quoted-string syntax variant, the value
+ // ; after quoted-string unescaping MUST conform to the
+ // ; 'token' ABNF.
+ //
+ // The limiting of parameter values to tokens or "quoted tokens" makes
+ // the parsing of the header significantly simpler and allows a number
+ // of short-cuts to be taken.
+
+ // Step one, split the header into individual extensions using ',' as a
+ // separator
+ String unparsedExtensions[] = header.split(",");
+ for (String unparsedExtension : unparsedExtensions) {
+ // Step two, split the extension into the registered name and
+ // parameter/value pairs using ';' as a separator
+ String unparsedParameters[] = unparsedExtension.split(";");
+ WsExtension extension = new WsExtension(unparsedParameters[0].trim());
+
+ for (int i = 1; i < unparsedParameters.length; i++) {
+ int equalsPos = unparsedParameters[i].indexOf('=');
+ String name;
+ String value;
+ if (equalsPos == -1) {
+ name = unparsedParameters[i].trim();
+ value = null;
+ } else {
+ name = unparsedParameters[i].substring(0, equalsPos).trim();
+ value = unparsedParameters[i].substring(equalsPos + 1).trim();
+ int len = value.length();
+ if (len > 1) {
+ if (value.charAt(0) == '\"' && value.charAt(len - 1) == '\"') {
+ value = value.substring(1, value.length() - 1);
+ }
+ }
+ }
+ // Make sure value doesn't contain any of the delimiters since
+ // that would indicate something went wrong
+ if (containsDelims(name) || containsDelims(value)) {
+ throw new IllegalArgumentException(sm.getString(
+ "util.notToken", name, value));
+ }
+ if (value != null &&
+ (value.indexOf(',') > -1 || value.indexOf(';') > -1 ||
+ value.indexOf('\"') > -1 || value.indexOf('=') > -1)) {
+ throw new IllegalArgumentException(sm.getString("", value));
+ }
+ extension.addParameter(new WsExtensionParameter(name, value));
+ }
+ extensions.add(extension);
+ }
+ }
+
+
+ private static boolean containsDelims(String input) {
+ if (input == null || input.length() == 0) {
+ return false;
+ }
+ for (char c : input.toCharArray()) {
+ switch (c) {
+ case ',':
+ case ';':
+ case '\"':
+ case '=':
+ return true;
+ default:
+ // NO_OP
+ }
+
+ }
+ return false;
+ }
+
+ private static Method getOnMessageMethod(MessageHandler listener) {
+ try {
+ return listener.getClass().getMethod("onMessage", Object.class);
+ } catch (NoSuchMethodException | SecurityException e) {
+ throw new IllegalArgumentException(
+ sm.getString("util.invalidMessageHandler"), e);
+ }
+ }
+
+ private static Method getOnMessagePartialMethod(MessageHandler listener) {
+ try {
+ return listener.getClass().getMethod("onMessage", Object.class, Boolean.TYPE);
+ } catch (NoSuchMethodException | SecurityException e) {
+ throw new IllegalArgumentException(
+ sm.getString("util.invalidMessageHandler"), e);
+ }
+ }
+
+
+ public static class DecoderMatch {
+
+ private final List<Class<? extends Decoder>> textDecoders =
+ new ArrayList<>();
+ private final List<Class<? extends Decoder>> binaryDecoders =
+ new ArrayList<>();
+ private final Class<?> target;
+
+ public DecoderMatch(Class<?> target, List<DecoderEntry> decoderEntries) {
+ this.target = target;
+ for (DecoderEntry decoderEntry : decoderEntries) {
+ if (decoderEntry.getClazz().isAssignableFrom(target)) {
+ if (Binary.class.isAssignableFrom(
+ decoderEntry.getDecoderClazz())) {
+ binaryDecoders.add(decoderEntry.getDecoderClazz());
+ // willDecode() method means this decoder may or may not
+ // decode a message so need to carry on checking for
+ // other matches
+ } else if (BinaryStream.class.isAssignableFrom(
+ decoderEntry.getDecoderClazz())) {
+ binaryDecoders.add(decoderEntry.getDecoderClazz());
+ // Stream decoders have to process the message so no
+ // more decoders can be matched
+ break;
+ } else if (Text.class.isAssignableFrom(
+ decoderEntry.getDecoderClazz())) {
+ textDecoders.add(decoderEntry.getDecoderClazz());
+ // willDecode() method means this decoder may or may not
+ // decode a message so need to carry on checking for
+ // other matches
+ } else if (TextStream.class.isAssignableFrom(
+ decoderEntry.getDecoderClazz())) {
+ textDecoders.add(decoderEntry.getDecoderClazz());
+ // Stream decoders have to process the message so no
+ // more decoders can be matched
+ break;
+ } else {
+ throw new IllegalArgumentException(
+ sm.getString("util.unknownDecoderType"));
+ }
+ }
+ }
+ }
+
+
+ public List<Class<? extends Decoder>> getTextDecoders() {
+ return textDecoders;
+ }
+
+
+ public List<Class<? extends Decoder>> getBinaryDecoders() {
+ return binaryDecoders;
+ }
+
+
+ public Class<?> getTarget() {
+ return target;
+ }
+
+
+ public boolean hasMatches() {
+ return (textDecoders.size() > 0) || (binaryDecoders.size() > 0);
+ }
+ }
+
+
+ private static class TypeResult {
+ private final Class<?> clazz;
+ private final int index;
+ private int dimension;
+
+ public TypeResult(Class<?> clazz, int index, int dimension) {
+ this.clazz= clazz;
+ this.index = index;
+ this.dimension = dimension;
+ }
+
+ public Class<?> getClazz() {
+ return clazz;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public int getDimension() {
+ return dimension;
+ }
+
+ public void incrementDimension(int inc) {
+ dimension += inc;
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/WrappedMessageHandler.java b/src/java/nginx/unit/websocket/WrappedMessageHandler.java
new file mode 100644
index 00000000..2557a73e
--- /dev/null
+++ b/src/java/nginx/unit/websocket/WrappedMessageHandler.java
@@ -0,0 +1,25 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import javax.websocket.MessageHandler;
+
+public interface WrappedMessageHandler {
+ long getMaxMessageSize();
+
+ MessageHandler getWrappedHandler();
+}
diff --git a/src/java/nginx/unit/websocket/WsContainerProvider.java b/src/java/nginx/unit/websocket/WsContainerProvider.java
new file mode 100644
index 00000000..f8a404a1
--- /dev/null
+++ b/src/java/nginx/unit/websocket/WsContainerProvider.java
@@ -0,0 +1,28 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import javax.websocket.ContainerProvider;
+import javax.websocket.WebSocketContainer;
+
+public class WsContainerProvider extends ContainerProvider {
+
+ @Override
+ protected WebSocketContainer getContainer() {
+ return new WsWebSocketContainer();
+ }
+}
diff --git a/src/java/nginx/unit/websocket/WsExtension.java b/src/java/nginx/unit/websocket/WsExtension.java
new file mode 100644
index 00000000..3846feb1
--- /dev/null
+++ b/src/java/nginx/unit/websocket/WsExtension.java
@@ -0,0 +1,46 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.websocket.Extension;
+
+public class WsExtension implements Extension {
+
+ private final String name;
+ private final List<Parameter> parameters = new ArrayList<>();
+
+ WsExtension(String name) {
+ this.name = name;
+ }
+
+ void addParameter(Parameter parameter) {
+ parameters.add(parameter);
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public List<Parameter> getParameters() {
+ return parameters;
+ }
+}
diff --git a/src/java/nginx/unit/websocket/WsExtensionParameter.java b/src/java/nginx/unit/websocket/WsExtensionParameter.java
new file mode 100644
index 00000000..9b82f1c7
--- /dev/null
+++ b/src/java/nginx/unit/websocket/WsExtensionParameter.java
@@ -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.
+ */
+package nginx.unit.websocket;
+
+import javax.websocket.Extension.Parameter;
+
+public class WsExtensionParameter implements Parameter {
+
+ private final String name;
+ private final String value;
+
+ WsExtensionParameter(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/src/java/nginx/unit/websocket/WsFrameBase.java b/src/java/nginx/unit/websocket/WsFrameBase.java
new file mode 100644
index 00000000..06d20bf4
--- /dev/null
+++ b/src/java/nginx/unit/websocket/WsFrameBase.java
@@ -0,0 +1,1010 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
+
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCodes;
+import javax.websocket.Extension;
+import javax.websocket.MessageHandler;
+import javax.websocket.PongMessage;
+
+import org.apache.juli.logging.Log;
+import org.apache.tomcat.util.ExceptionUtils;
+import org.apache.tomcat.util.buf.Utf8Decoder;
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * Takes the ServletInputStream, processes the WebSocket frames it contains and
+ * extracts the messages. WebSocket Pings received will be responded to
+ * automatically without any action required by the application.
+ */
+public abstract class WsFrameBase {
+
+ private static final StringManager sm = StringManager.getManager(WsFrameBase.class);
+
+ // Connection level attributes
+ protected final WsSession wsSession;
+ protected final ByteBuffer inputBuffer;
+ private final Transformation transformation;
+
+ // Attributes for control messages
+ // Control messages can appear in the middle of other messages so need
+ // separate attributes
+ private final ByteBuffer controlBufferBinary = ByteBuffer.allocate(125);
+ private final CharBuffer controlBufferText = CharBuffer.allocate(125);
+
+ // Attributes of the current message
+ private final CharsetDecoder utf8DecoderControl = new Utf8Decoder().
+ onMalformedInput(CodingErrorAction.REPORT).
+ onUnmappableCharacter(CodingErrorAction.REPORT);
+ private final CharsetDecoder utf8DecoderMessage = new Utf8Decoder().
+ onMalformedInput(CodingErrorAction.REPORT).
+ onUnmappableCharacter(CodingErrorAction.REPORT);
+ private boolean continuationExpected = false;
+ private boolean textMessage = false;
+ private ByteBuffer messageBufferBinary;
+ private CharBuffer messageBufferText;
+ // Cache the message handler in force when the message starts so it is used
+ // consistently for the entire message
+ private MessageHandler binaryMsgHandler = null;
+ private MessageHandler textMsgHandler = null;
+
+ // Attributes of the current frame
+ private boolean fin = false;
+ private int rsv = 0;
+ private byte opCode = 0;
+ private final byte[] mask = new byte[4];
+ private int maskIndex = 0;
+ private long payloadLength = 0;
+ private volatile long payloadWritten = 0;
+
+ // Attributes tracking state
+ private volatile State state = State.NEW_FRAME;
+ private volatile boolean open = true;
+
+ private static final AtomicReferenceFieldUpdater<WsFrameBase, ReadState> READ_STATE_UPDATER =
+ AtomicReferenceFieldUpdater.newUpdater(WsFrameBase.class, ReadState.class, "readState");
+ private volatile ReadState readState = ReadState.WAITING;
+
+ public WsFrameBase(WsSession wsSession, Transformation transformation) {
+ inputBuffer = ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE);
+ inputBuffer.position(0).limit(0);
+ messageBufferBinary = ByteBuffer.allocate(wsSession.getMaxBinaryMessageBufferSize());
+ messageBufferText = CharBuffer.allocate(wsSession.getMaxTextMessageBufferSize());
+ this.wsSession = wsSession;
+ Transformation finalTransformation;
+ if (isMasked()) {
+ finalTransformation = new UnmaskTransformation();
+ } else {
+ finalTransformation = new NoopTransformation();
+ }
+ if (transformation == null) {
+ this.transformation = finalTransformation;
+ } else {
+ transformation.setNext(finalTransformation);
+ this.transformation = transformation;
+ }
+ }
+
+
+ protected void processInputBuffer() throws IOException {
+ while (!isSuspended()) {
+ wsSession.updateLastActive();
+ if (state == State.NEW_FRAME) {
+ if (!processInitialHeader()) {
+ break;
+ }
+ // If a close frame has been received, no further data should
+ // have seen
+ if (!open) {
+ throw new IOException(sm.getString("wsFrame.closed"));
+ }
+ }
+ if (state == State.PARTIAL_HEADER) {
+ if (!processRemainingHeader()) {
+ break;
+ }
+ }
+ if (state == State.DATA) {
+ if (!processData()) {
+ break;
+ }
+ }
+ }
+ }
+
+
+ /**
+ * @return <code>true</code> if sufficient data was present to process all
+ * of the initial header
+ */
+ private boolean processInitialHeader() throws IOException {
+ // Need at least two bytes of data to do this
+ if (inputBuffer.remaining() < 2) {
+ return false;
+ }
+ int b = inputBuffer.get();
+ fin = (b & 0x80) != 0;
+ rsv = (b & 0x70) >>> 4;
+ opCode = (byte) (b & 0x0F);
+ if (!transformation.validateRsv(rsv, opCode)) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ sm.getString("wsFrame.wrongRsv", Integer.valueOf(rsv), Integer.valueOf(opCode))));
+ }
+
+ if (Util.isControl(opCode)) {
+ if (!fin) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ sm.getString("wsFrame.controlFragmented")));
+ }
+ if (opCode != Constants.OPCODE_PING &&
+ opCode != Constants.OPCODE_PONG &&
+ opCode != Constants.OPCODE_CLOSE) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ sm.getString("wsFrame.invalidOpCode", Integer.valueOf(opCode))));
+ }
+ } else {
+ if (continuationExpected) {
+ if (!Util.isContinuation(opCode)) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ sm.getString("wsFrame.noContinuation")));
+ }
+ } else {
+ try {
+ if (opCode == Constants.OPCODE_BINARY) {
+ // New binary message
+ textMessage = false;
+ int size = wsSession.getMaxBinaryMessageBufferSize();
+ if (size != messageBufferBinary.capacity()) {
+ messageBufferBinary = ByteBuffer.allocate(size);
+ }
+ binaryMsgHandler = wsSession.getBinaryMessageHandler();
+ textMsgHandler = null;
+ } else if (opCode == Constants.OPCODE_TEXT) {
+ // New text message
+ textMessage = true;
+ int size = wsSession.getMaxTextMessageBufferSize();
+ if (size != messageBufferText.capacity()) {
+ messageBufferText = CharBuffer.allocate(size);
+ }
+ binaryMsgHandler = null;
+ textMsgHandler = wsSession.getTextMessageHandler();
+ } else {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ sm.getString("wsFrame.invalidOpCode", Integer.valueOf(opCode))));
+ }
+ } catch (IllegalStateException ise) {
+ // Thrown if the session is already closed
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ sm.getString("wsFrame.sessionClosed")));
+ }
+ }
+ continuationExpected = !fin;
+ }
+ b = inputBuffer.get();
+ // Client data must be masked
+ if ((b & 0x80) == 0 && isMasked()) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ sm.getString("wsFrame.notMasked")));
+ }
+ payloadLength = b & 0x7F;
+ state = State.PARTIAL_HEADER;
+ if (getLog().isDebugEnabled()) {
+ getLog().debug(sm.getString("wsFrame.partialHeaderComplete", Boolean.toString(fin),
+ Integer.toString(rsv), Integer.toString(opCode), Long.toString(payloadLength)));
+ }
+ return true;
+ }
+
+
+ protected abstract boolean isMasked();
+ protected abstract Log getLog();
+
+
+ /**
+ * @return <code>true</code> if sufficient data was present to complete the
+ * processing of the header
+ */
+ private boolean processRemainingHeader() throws IOException {
+ // Ignore the 2 bytes already read. 4 for the mask
+ int headerLength;
+ if (isMasked()) {
+ headerLength = 4;
+ } else {
+ headerLength = 0;
+ }
+ // Add additional bytes depending on length
+ if (payloadLength == 126) {
+ headerLength += 2;
+ } else if (payloadLength == 127) {
+ headerLength += 8;
+ }
+ if (inputBuffer.remaining() < headerLength) {
+ return false;
+ }
+ // Calculate new payload length if necessary
+ if (payloadLength == 126) {
+ payloadLength = byteArrayToLong(inputBuffer.array(),
+ inputBuffer.arrayOffset() + inputBuffer.position(), 2);
+ inputBuffer.position(inputBuffer.position() + 2);
+ } else if (payloadLength == 127) {
+ payloadLength = byteArrayToLong(inputBuffer.array(),
+ inputBuffer.arrayOffset() + inputBuffer.position(), 8);
+ inputBuffer.position(inputBuffer.position() + 8);
+ }
+ if (Util.isControl(opCode)) {
+ if (payloadLength > 125) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ sm.getString("wsFrame.controlPayloadTooBig", Long.valueOf(payloadLength))));
+ }
+ if (!fin) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ sm.getString("wsFrame.controlNoFin")));
+ }
+ }
+ if (isMasked()) {
+ inputBuffer.get(mask, 0, 4);
+ }
+ state = State.DATA;
+ return true;
+ }
+
+
+ private boolean processData() throws IOException {
+ boolean result;
+ if (Util.isControl(opCode)) {
+ result = processDataControl();
+ } else if (textMessage) {
+ if (textMsgHandler == null) {
+ result = swallowInput();
+ } else {
+ result = processDataText();
+ }
+ } else {
+ if (binaryMsgHandler == null) {
+ result = swallowInput();
+ } else {
+ result = processDataBinary();
+ }
+ }
+ checkRoomPayload();
+ return result;
+ }
+
+
+ private boolean processDataControl() throws IOException {
+ TransformationResult tr = transformation.getMoreData(opCode, fin, rsv, controlBufferBinary);
+ if (TransformationResult.UNDERFLOW.equals(tr)) {
+ return false;
+ }
+ // Control messages have fixed message size so
+ // TransformationResult.OVERFLOW is not possible here
+
+ controlBufferBinary.flip();
+ if (opCode == Constants.OPCODE_CLOSE) {
+ open = false;
+ String reason = null;
+ int code = CloseCodes.NORMAL_CLOSURE.getCode();
+ if (controlBufferBinary.remaining() == 1) {
+ controlBufferBinary.clear();
+ // Payload must be zero or 2+ bytes long
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ sm.getString("wsFrame.oneByteCloseCode")));
+ }
+ if (controlBufferBinary.remaining() > 1) {
+ code = controlBufferBinary.getShort();
+ if (controlBufferBinary.remaining() > 0) {
+ CoderResult cr = utf8DecoderControl.decode(controlBufferBinary,
+ controlBufferText, true);
+ if (cr.isError()) {
+ controlBufferBinary.clear();
+ controlBufferText.clear();
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ sm.getString("wsFrame.invalidUtf8Close")));
+ }
+ // There will be no overflow as the output buffer is big
+ // enough. There will be no underflow as all the data is
+ // passed to the decoder in a single call.
+ controlBufferText.flip();
+ reason = controlBufferText.toString();
+ }
+ }
+ wsSession.onClose(new CloseReason(Util.getCloseCode(code), reason));
+ } else if (opCode == Constants.OPCODE_PING) {
+ if (wsSession.isOpen()) {
+ wsSession.getBasicRemote().sendPong(controlBufferBinary);
+ }
+ } else if (opCode == Constants.OPCODE_PONG) {
+ MessageHandler.Whole<PongMessage> mhPong = wsSession.getPongMessageHandler();
+ if (mhPong != null) {
+ try {
+ mhPong.onMessage(new WsPongMessage(controlBufferBinary));
+ } catch (Throwable t) {
+ handleThrowableOnSend(t);
+ } finally {
+ controlBufferBinary.clear();
+ }
+ }
+ } else {
+ // Should have caught this earlier but just in case...
+ controlBufferBinary.clear();
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ sm.getString("wsFrame.invalidOpCode", Integer.valueOf(opCode))));
+ }
+ controlBufferBinary.clear();
+ newFrame();
+ return true;
+ }
+
+
+ @SuppressWarnings("unchecked")
+ protected void sendMessageText(boolean last) throws WsIOException {
+ if (textMsgHandler instanceof WrappedMessageHandler) {
+ long maxMessageSize = ((WrappedMessageHandler) textMsgHandler).getMaxMessageSize();
+ if (maxMessageSize > -1 && messageBufferText.remaining() > maxMessageSize) {
+ throw new WsIOException(new CloseReason(CloseCodes.TOO_BIG,
+ sm.getString("wsFrame.messageTooBig",
+ Long.valueOf(messageBufferText.remaining()),
+ Long.valueOf(maxMessageSize))));
+ }
+ }
+
+ try {
+ if (textMsgHandler instanceof MessageHandler.Partial<?>) {
+ ((MessageHandler.Partial<String>) textMsgHandler)
+ .onMessage(messageBufferText.toString(), last);
+ } else {
+ // Caller ensures last == true if this branch is used
+ ((MessageHandler.Whole<String>) textMsgHandler)
+ .onMessage(messageBufferText.toString());
+ }
+ } catch (Throwable t) {
+ handleThrowableOnSend(t);
+ } finally {
+ messageBufferText.clear();
+ }
+ }
+
+
+ private boolean processDataText() throws IOException {
+ // Copy the available data to the buffer
+ TransformationResult tr = transformation.getMoreData(opCode, fin, rsv, messageBufferBinary);
+ while (!TransformationResult.END_OF_FRAME.equals(tr)) {
+ // Frame not complete - we ran out of something
+ // Convert bytes to UTF-8
+ messageBufferBinary.flip();
+ while (true) {
+ CoderResult cr = utf8DecoderMessage.decode(messageBufferBinary, messageBufferText,
+ false);
+ if (cr.isError()) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.NOT_CONSISTENT,
+ sm.getString("wsFrame.invalidUtf8")));
+ } else if (cr.isOverflow()) {
+ // Ran out of space in text buffer - flush it
+ if (usePartial()) {
+ messageBufferText.flip();
+ sendMessageText(false);
+ messageBufferText.clear();
+ } else {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.TOO_BIG,
+ sm.getString("wsFrame.textMessageTooBig")));
+ }
+ } else if (cr.isUnderflow()) {
+ // Compact what we have to create as much space as possible
+ messageBufferBinary.compact();
+
+ // Need more input
+ // What did we run out of?
+ if (TransformationResult.OVERFLOW.equals(tr)) {
+ // Ran out of message buffer - exit inner loop and
+ // refill
+ break;
+ } else {
+ // TransformationResult.UNDERFLOW
+ // Ran out of input data - get some more
+ return false;
+ }
+ }
+ }
+ // Read more input data
+ tr = transformation.getMoreData(opCode, fin, rsv, messageBufferBinary);
+ }
+
+ messageBufferBinary.flip();
+ boolean last = false;
+ // Frame is fully received
+ // Convert bytes to UTF-8
+ while (true) {
+ CoderResult cr = utf8DecoderMessage.decode(messageBufferBinary, messageBufferText,
+ last);
+ if (cr.isError()) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.NOT_CONSISTENT,
+ sm.getString("wsFrame.invalidUtf8")));
+ } else if (cr.isOverflow()) {
+ // Ran out of space in text buffer - flush it
+ if (usePartial()) {
+ messageBufferText.flip();
+ sendMessageText(false);
+ messageBufferText.clear();
+ } else {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.TOO_BIG,
+ sm.getString("wsFrame.textMessageTooBig")));
+ }
+ } else if (cr.isUnderflow() && !last) {
+ // End of frame and possible message as well.
+
+ if (continuationExpected) {
+ // If partial messages are supported, send what we have
+ // managed to decode
+ if (usePartial()) {
+ messageBufferText.flip();
+ sendMessageText(false);
+ messageBufferText.clear();
+ }
+ messageBufferBinary.compact();
+ newFrame();
+ // Process next frame
+ return true;
+ } else {
+ // Make sure coder has flushed all output
+ last = true;
+ }
+ } else {
+ // End of message
+ messageBufferText.flip();
+ sendMessageText(true);
+
+ newMessage();
+ return true;
+ }
+ }
+ }
+
+
+ private boolean processDataBinary() throws IOException {
+ // Copy the available data to the buffer
+ TransformationResult tr = transformation.getMoreData(opCode, fin, rsv, messageBufferBinary);
+ while (!TransformationResult.END_OF_FRAME.equals(tr)) {
+ // Frame not complete - what did we run out of?
+ if (TransformationResult.UNDERFLOW.equals(tr)) {
+ // Ran out of input data - get some more
+ return false;
+ }
+
+ // Ran out of message buffer - flush it
+ if (!usePartial()) {
+ CloseReason cr = new CloseReason(CloseCodes.TOO_BIG,
+ sm.getString("wsFrame.bufferTooSmall",
+ Integer.valueOf(messageBufferBinary.capacity()),
+ Long.valueOf(payloadLength)));
+ throw new WsIOException(cr);
+ }
+ messageBufferBinary.flip();
+ ByteBuffer copy = ByteBuffer.allocate(messageBufferBinary.limit());
+ copy.put(messageBufferBinary);
+ copy.flip();
+ sendMessageBinary(copy, false);
+ messageBufferBinary.clear();
+ // Read more data
+ tr = transformation.getMoreData(opCode, fin, rsv, messageBufferBinary);
+ }
+
+ // Frame is fully received
+ // Send the message if either:
+ // - partial messages are supported
+ // - the message is complete
+ if (usePartial() || !continuationExpected) {
+ messageBufferBinary.flip();
+ ByteBuffer copy = ByteBuffer.allocate(messageBufferBinary.limit());
+ copy.put(messageBufferBinary);
+ copy.flip();
+ sendMessageBinary(copy, !continuationExpected);
+ messageBufferBinary.clear();
+ }
+
+ if (continuationExpected) {
+ // More data for this message expected, start a new frame
+ newFrame();
+ } else {
+ // Message is complete, start a new message
+ newMessage();
+ }
+
+ return true;
+ }
+
+
+ private void handleThrowableOnSend(Throwable t) throws WsIOException {
+ ExceptionUtils.handleThrowable(t);
+ wsSession.getLocal().onError(wsSession, t);
+ CloseReason cr = new CloseReason(CloseCodes.CLOSED_ABNORMALLY,
+ sm.getString("wsFrame.ioeTriggeredClose"));
+ throw new WsIOException(cr);
+ }
+
+
+ @SuppressWarnings("unchecked")
+ protected void sendMessageBinary(ByteBuffer msg, boolean last) throws WsIOException {
+ if (binaryMsgHandler instanceof WrappedMessageHandler) {
+ long maxMessageSize = ((WrappedMessageHandler) binaryMsgHandler).getMaxMessageSize();
+ if (maxMessageSize > -1 && msg.remaining() > maxMessageSize) {
+ throw new WsIOException(new CloseReason(CloseCodes.TOO_BIG,
+ sm.getString("wsFrame.messageTooBig",
+ Long.valueOf(msg.remaining()),
+ Long.valueOf(maxMessageSize))));
+ }
+ }
+ try {
+ if (binaryMsgHandler instanceof MessageHandler.Partial<?>) {
+ ((MessageHandler.Partial<ByteBuffer>) binaryMsgHandler).onMessage(msg, last);
+ } else {
+ // Caller ensures last == true if this branch is used
+ ((MessageHandler.Whole<ByteBuffer>) binaryMsgHandler).onMessage(msg);
+ }
+ } catch (Throwable t) {
+ handleThrowableOnSend(t);
+ }
+ }
+
+
+ private void newMessage() {
+ messageBufferBinary.clear();
+ messageBufferText.clear();
+ utf8DecoderMessage.reset();
+ continuationExpected = false;
+ newFrame();
+ }
+
+
+ private void newFrame() {
+ if (inputBuffer.remaining() == 0) {
+ inputBuffer.position(0).limit(0);
+ }
+
+ maskIndex = 0;
+ payloadWritten = 0;
+ state = State.NEW_FRAME;
+
+ // These get reset in processInitialHeader()
+ // fin, rsv, opCode, payloadLength, mask
+
+ checkRoomHeaders();
+ }
+
+
+ private void checkRoomHeaders() {
+ // Is the start of the current frame too near the end of the input
+ // buffer?
+ if (inputBuffer.capacity() - inputBuffer.position() < 131) {
+ // Limit based on a control frame with a full payload
+ makeRoom();
+ }
+ }
+
+
+ private void checkRoomPayload() {
+ if (inputBuffer.capacity() - inputBuffer.position() - payloadLength + payloadWritten < 0) {
+ makeRoom();
+ }
+ }
+
+
+ private void makeRoom() {
+ inputBuffer.compact();
+ inputBuffer.flip();
+ }
+
+
+ private boolean usePartial() {
+ if (Util.isControl(opCode)) {
+ return false;
+ } else if (textMessage) {
+ return textMsgHandler instanceof MessageHandler.Partial;
+ } else {
+ // Must be binary
+ return binaryMsgHandler instanceof MessageHandler.Partial;
+ }
+ }
+
+
+ private boolean swallowInput() {
+ long toSkip = Math.min(payloadLength - payloadWritten, inputBuffer.remaining());
+ inputBuffer.position(inputBuffer.position() + (int) toSkip);
+ payloadWritten += toSkip;
+ if (payloadWritten == payloadLength) {
+ if (continuationExpected) {
+ newFrame();
+ } else {
+ newMessage();
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+
+ protected static long byteArrayToLong(byte[] b, int start, int len) throws IOException {
+ if (len > 8) {
+ throw new IOException(sm.getString("wsFrame.byteToLongFail", Long.valueOf(len)));
+ }
+ int shift = 0;
+ long result = 0;
+ for (int i = start + len - 1; i >= start; i--) {
+ result = result + ((b[i] & 0xFF) << shift);
+ shift += 8;
+ }
+ return result;
+ }
+
+
+ protected boolean isOpen() {
+ return open;
+ }
+
+
+ protected Transformation getTransformation() {
+ return transformation;
+ }
+
+
+ private enum State {
+ NEW_FRAME, PARTIAL_HEADER, DATA
+ }
+
+
+ /**
+ * WAITING - not suspended
+ * Server case: waiting for a notification that data
+ * is ready to be read from the socket, the socket is
+ * registered to the poller
+ * Client case: data has been read from the socket and
+ * is waiting for data to be processed
+ * PROCESSING - not suspended
+ * Server case: reading from the socket and processing
+ * the data
+ * Client case: processing the data if such has
+ * already been read and more data will be read from
+ * the socket
+ * SUSPENDING_WAIT - suspended, a call to suspend() was made while in
+ * WAITING state. A call to resume() will do nothing
+ * and will transition to WAITING state
+ * SUSPENDING_PROCESS - suspended, a call to suspend() was made while in
+ * PROCESSING state. A call to resume() will do
+ * nothing and will transition to PROCESSING state
+ * SUSPENDED - suspended
+ * Server case: processing data finished
+ * (SUSPENDING_PROCESS) / a notification was received
+ * that data is ready to be read from the socket
+ * (SUSPENDING_WAIT), socket is not registered to the
+ * poller
+ * Client case: processing data finished
+ * (SUSPENDING_PROCESS) / data has been read from the
+ * socket and is available for processing
+ * (SUSPENDING_WAIT)
+ * A call to resume() will:
+ * Server case: register the socket to the poller
+ * Client case: resume data processing
+ * CLOSING - not suspended, a close will be send
+ *
+ * <pre>
+ * resume data to be resume
+ * no action processed no action
+ * |---------------| |---------------| |----------|
+ * | v | v v |
+ * | |----------WAITING --------PROCESSING----| |
+ * | | ^ processing | |
+ * | | | finished | |
+ * | | | | |
+ * | suspend | suspend |
+ * | | | | |
+ * | | resume | |
+ * | | register socket to poller (server) | |
+ * | | resume data processing (client) | |
+ * | | | | |
+ * | v | v |
+ * SUSPENDING_WAIT | SUSPENDING_PROCESS
+ * | | |
+ * | data available | processing finished |
+ * |------------- SUSPENDED ----------------------|
+ * </pre>
+ */
+ protected enum ReadState {
+ WAITING (false),
+ PROCESSING (false),
+ SUSPENDING_WAIT (true),
+ SUSPENDING_PROCESS(true),
+ SUSPENDED (true),
+ CLOSING (false);
+
+ private final boolean isSuspended;
+
+ ReadState(boolean isSuspended) {
+ this.isSuspended = isSuspended;
+ }
+
+ public boolean isSuspended() {
+ return isSuspended;
+ }
+ }
+
+ public void suspend() {
+ while (true) {
+ switch (readState) {
+ case WAITING:
+ if (!READ_STATE_UPDATER.compareAndSet(this, ReadState.WAITING,
+ ReadState.SUSPENDING_WAIT)) {
+ continue;
+ }
+ return;
+ case PROCESSING:
+ if (!READ_STATE_UPDATER.compareAndSet(this, ReadState.PROCESSING,
+ ReadState.SUSPENDING_PROCESS)) {
+ continue;
+ }
+ return;
+ case SUSPENDING_WAIT:
+ if (readState != ReadState.SUSPENDING_WAIT) {
+ continue;
+ } else {
+ if (getLog().isWarnEnabled()) {
+ getLog().warn(sm.getString("wsFrame.suspendRequested"));
+ }
+ }
+ return;
+ case SUSPENDING_PROCESS:
+ if (readState != ReadState.SUSPENDING_PROCESS) {
+ continue;
+ } else {
+ if (getLog().isWarnEnabled()) {
+ getLog().warn(sm.getString("wsFrame.suspendRequested"));
+ }
+ }
+ return;
+ case SUSPENDED:
+ if (readState != ReadState.SUSPENDED) {
+ continue;
+ } else {
+ if (getLog().isWarnEnabled()) {
+ getLog().warn(sm.getString("wsFrame.alreadySuspended"));
+ }
+ }
+ return;
+ case CLOSING:
+ return;
+ default:
+ throw new IllegalStateException(sm.getString("wsFrame.illegalReadState", state));
+ }
+ }
+ }
+
+ public void resume() {
+ while (true) {
+ switch (readState) {
+ case WAITING:
+ if (readState != ReadState.WAITING) {
+ continue;
+ } else {
+ if (getLog().isWarnEnabled()) {
+ getLog().warn(sm.getString("wsFrame.alreadyResumed"));
+ }
+ }
+ return;
+ case PROCESSING:
+ if (readState != ReadState.PROCESSING) {
+ continue;
+ } else {
+ if (getLog().isWarnEnabled()) {
+ getLog().warn(sm.getString("wsFrame.alreadyResumed"));
+ }
+ }
+ return;
+ case SUSPENDING_WAIT:
+ if (!READ_STATE_UPDATER.compareAndSet(this, ReadState.SUSPENDING_WAIT,
+ ReadState.WAITING)) {
+ continue;
+ }
+ return;
+ case SUSPENDING_PROCESS:
+ if (!READ_STATE_UPDATER.compareAndSet(this, ReadState.SUSPENDING_PROCESS,
+ ReadState.PROCESSING)) {
+ continue;
+ }
+ return;
+ case SUSPENDED:
+ if (!READ_STATE_UPDATER.compareAndSet(this, ReadState.SUSPENDED,
+ ReadState.WAITING)) {
+ continue;
+ }
+ resumeProcessing();
+ return;
+ case CLOSING:
+ return;
+ default:
+ throw new IllegalStateException(sm.getString("wsFrame.illegalReadState", state));
+ }
+ }
+ }
+
+ protected boolean isSuspended() {
+ return readState.isSuspended();
+ }
+
+ protected ReadState getReadState() {
+ return readState;
+ }
+
+ protected void changeReadState(ReadState newState) {
+ READ_STATE_UPDATER.set(this, newState);
+ }
+
+ protected boolean changeReadState(ReadState oldState, ReadState newState) {
+ return READ_STATE_UPDATER.compareAndSet(this, oldState, newState);
+ }
+
+ /**
+ * This method will be invoked when the read operation is resumed.
+ * As the suspend of the read operation can be invoked at any time, when
+ * implementing this method one should consider that there might still be
+ * data remaining into the internal buffers that needs to be processed
+ * before reading again from the socket.
+ */
+ protected abstract void resumeProcessing();
+
+
+ private abstract class TerminalTransformation implements Transformation {
+
+ @Override
+ public boolean validateRsvBits(int i) {
+ // Terminal transformations don't use RSV bits and there is no next
+ // transformation so always return true.
+ return true;
+ }
+
+ @Override
+ public Extension getExtensionResponse() {
+ // Return null since terminal transformations are not extensions
+ return null;
+ }
+
+ @Override
+ public void setNext(Transformation t) {
+ // NO-OP since this is the terminal transformation
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Anything other than a value of zero for rsv is invalid.
+ */
+ @Override
+ public boolean validateRsv(int rsv, byte opCode) {
+ return rsv == 0;
+ }
+
+ @Override
+ public void close() {
+ // NO-OP for the terminal transformations
+ }
+ }
+
+
+ /**
+ * For use by the client implementation that needs to obtain payload data
+ * without the need for unmasking.
+ */
+ private final class NoopTransformation extends TerminalTransformation {
+
+ @Override
+ public TransformationResult getMoreData(byte opCode, boolean fin, int rsv,
+ ByteBuffer dest) {
+ // opCode is ignored as the transformation is the same for all
+ // opCodes
+ // rsv is ignored as it known to be zero at this point
+ long toWrite = Math.min(payloadLength - payloadWritten, inputBuffer.remaining());
+ toWrite = Math.min(toWrite, dest.remaining());
+
+ int orgLimit = inputBuffer.limit();
+ inputBuffer.limit(inputBuffer.position() + (int) toWrite);
+ dest.put(inputBuffer);
+ inputBuffer.limit(orgLimit);
+ payloadWritten += toWrite;
+
+ if (payloadWritten == payloadLength) {
+ return TransformationResult.END_OF_FRAME;
+ } else if (inputBuffer.remaining() == 0) {
+ return TransformationResult.UNDERFLOW;
+ } else {
+ // !dest.hasRemaining()
+ return TransformationResult.OVERFLOW;
+ }
+ }
+
+
+ @Override
+ public List<MessagePart> sendMessagePart(List<MessagePart> messageParts) {
+ // TODO Masking should move to this method
+ // NO-OP send so simply return the message unchanged.
+ return messageParts;
+ }
+ }
+
+
+ /**
+ * For use by the server implementation that needs to obtain payload data
+ * and unmask it before any further processing.
+ */
+ private final class UnmaskTransformation extends TerminalTransformation {
+
+ @Override
+ public TransformationResult getMoreData(byte opCode, boolean fin, int rsv,
+ ByteBuffer dest) {
+ // opCode is ignored as the transformation is the same for all
+ // opCodes
+ // rsv is ignored as it known to be zero at this point
+ while (payloadWritten < payloadLength && inputBuffer.remaining() > 0 &&
+ dest.hasRemaining()) {
+ byte b = (byte) ((inputBuffer.get() ^ mask[maskIndex]) & 0xFF);
+ maskIndex++;
+ if (maskIndex == 4) {
+ maskIndex = 0;
+ }
+ payloadWritten++;
+ dest.put(b);
+ }
+ if (payloadWritten == payloadLength) {
+ return TransformationResult.END_OF_FRAME;
+ } else if (inputBuffer.remaining() == 0) {
+ return TransformationResult.UNDERFLOW;
+ } else {
+ // !dest.hasRemaining()
+ return TransformationResult.OVERFLOW;
+ }
+ }
+
+ @Override
+ public List<MessagePart> sendMessagePart(List<MessagePart> messageParts) {
+ // NO-OP send so simply return the message unchanged.
+ return messageParts;
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/WsFrameClient.java b/src/java/nginx/unit/websocket/WsFrameClient.java
new file mode 100644
index 00000000..3174c766
--- /dev/null
+++ b/src/java/nginx/unit/websocket/WsFrameClient.java
@@ -0,0 +1,228 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.CompletionHandler;
+
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCodes;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.res.StringManager;
+
+public class WsFrameClient extends WsFrameBase {
+
+ private final Log log = LogFactory.getLog(WsFrameClient.class); // must not be static
+ private static final StringManager sm = StringManager.getManager(WsFrameClient.class);
+
+ private final AsyncChannelWrapper channel;
+ private final CompletionHandler<Integer, Void> handler;
+ // Not final as it may need to be re-sized
+ private volatile ByteBuffer response;
+
+ public WsFrameClient(ByteBuffer response, AsyncChannelWrapper channel, WsSession wsSession,
+ Transformation transformation) {
+ super(wsSession, transformation);
+ this.response = response;
+ this.channel = channel;
+ this.handler = new WsFrameClientCompletionHandler();
+ }
+
+
+ void startInputProcessing() {
+ try {
+ processSocketRead();
+ } catch (IOException e) {
+ close(e);
+ }
+ }
+
+
+ private void processSocketRead() throws IOException {
+ while (true) {
+ switch (getReadState()) {
+ case WAITING:
+ if (!changeReadState(ReadState.WAITING, ReadState.PROCESSING)) {
+ continue;
+ }
+ while (response.hasRemaining()) {
+ if (isSuspended()) {
+ if (!changeReadState(ReadState.SUSPENDING_PROCESS, ReadState.SUSPENDED)) {
+ continue;
+ }
+ // There is still data available in the response buffer
+ // Return here so that the response buffer will not be
+ // cleared and there will be no data read from the
+ // socket. Thus when the read operation is resumed first
+ // the data left in the response buffer will be consumed
+ // and then a new socket read will be performed
+ return;
+ }
+ inputBuffer.mark();
+ inputBuffer.position(inputBuffer.limit()).limit(inputBuffer.capacity());
+
+ int toCopy = Math.min(response.remaining(), inputBuffer.remaining());
+
+ // Copy remaining bytes read in HTTP phase to input buffer used by
+ // frame processing
+
+ int orgLimit = response.limit();
+ response.limit(response.position() + toCopy);
+ inputBuffer.put(response);
+ response.limit(orgLimit);
+
+ inputBuffer.limit(inputBuffer.position()).reset();
+
+ // Process the data we have
+ processInputBuffer();
+ }
+ response.clear();
+
+ // Get some more data
+ if (isOpen()) {
+ channel.read(response, null, handler);
+ } else {
+ changeReadState(ReadState.CLOSING);
+ }
+ return;
+ case SUSPENDING_WAIT:
+ if (!changeReadState(ReadState.SUSPENDING_WAIT, ReadState.SUSPENDED)) {
+ continue;
+ }
+ return;
+ default:
+ throw new IllegalStateException(
+ sm.getString("wsFrameServer.illegalReadState", getReadState()));
+ }
+ }
+ }
+
+
+ private final void close(Throwable t) {
+ changeReadState(ReadState.CLOSING);
+ CloseReason cr;
+ if (t instanceof WsIOException) {
+ cr = ((WsIOException) t).getCloseReason();
+ } else {
+ cr = new CloseReason(CloseCodes.CLOSED_ABNORMALLY, t.getMessage());
+ }
+
+ try {
+ wsSession.close(cr);
+ } catch (IOException ignore) {
+ // Ignore
+ }
+ }
+
+
+ @Override
+ protected boolean isMasked() {
+ // Data is from the server so it is not masked
+ return false;
+ }
+
+
+ @Override
+ protected Log getLog() {
+ return log;
+ }
+
+ private class WsFrameClientCompletionHandler implements CompletionHandler<Integer, Void> {
+
+ @Override
+ public void completed(Integer result, Void attachment) {
+ if (result.intValue() == -1) {
+ // BZ 57762. A dropped connection will get reported as EOF
+ // rather than as an error so handle it here.
+ if (isOpen()) {
+ // No close frame was received
+ close(new EOFException());
+ }
+ // No data to process
+ return;
+ }
+ response.flip();
+ doResumeProcessing(true);
+ }
+
+ @Override
+ public void failed(Throwable exc, Void attachment) {
+ if (exc instanceof ReadBufferOverflowException) {
+ // response will be empty if this exception is thrown
+ response = ByteBuffer
+ .allocate(((ReadBufferOverflowException) exc).getMinBufferSize());
+ response.flip();
+ doResumeProcessing(false);
+ } else {
+ close(exc);
+ }
+ }
+
+ private void doResumeProcessing(boolean checkOpenOnError) {
+ while (true) {
+ switch (getReadState()) {
+ case PROCESSING:
+ if (!changeReadState(ReadState.PROCESSING, ReadState.WAITING)) {
+ continue;
+ }
+ resumeProcessing(checkOpenOnError);
+ return;
+ case SUSPENDING_PROCESS:
+ if (!changeReadState(ReadState.SUSPENDING_PROCESS, ReadState.SUSPENDED)) {
+ continue;
+ }
+ return;
+ default:
+ throw new IllegalStateException(
+ sm.getString("wsFrame.illegalReadState", getReadState()));
+ }
+ }
+ }
+ }
+
+
+ @Override
+ protected void resumeProcessing() {
+ resumeProcessing(true);
+ }
+
+ private void resumeProcessing(boolean checkOpenOnError) {
+ try {
+ processSocketRead();
+ } catch (IOException e) {
+ if (checkOpenOnError) {
+ // Only send a close message on an IOException if the client
+ // has not yet received a close control message from the server
+ // as the IOException may be in response to the client
+ // continuing to send a message after the server sent a close
+ // control message.
+ if (isOpen()) {
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("wsFrameClient.ioe"), e);
+ }
+ close(e);
+ }
+ } else {
+ close(e);
+ }
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/WsHandshakeResponse.java b/src/java/nginx/unit/websocket/WsHandshakeResponse.java
new file mode 100644
index 00000000..6e57ffd5
--- /dev/null
+++ b/src/java/nginx/unit/websocket/WsHandshakeResponse.java
@@ -0,0 +1,56 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.websocket.HandshakeResponse;
+
+import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap;
+
+/**
+ * Represents the response to a WebSocket handshake.
+ */
+public class WsHandshakeResponse implements HandshakeResponse {
+
+ private final Map<String,List<String>> headers = new CaseInsensitiveKeyMap<>();
+
+
+ public WsHandshakeResponse() {
+ }
+
+
+ public WsHandshakeResponse(Map<String,List<String>> headers) {
+ for (Entry<String,List<String>> entry : headers.entrySet()) {
+ if (this.headers.containsKey(entry.getKey())) {
+ this.headers.get(entry.getKey()).addAll(entry.getValue());
+ } else {
+ List<String> values = new ArrayList<>(entry.getValue());
+ this.headers.put(entry.getKey(), values);
+ }
+ }
+ }
+
+
+ @Override
+ public Map<String,List<String>> getHeaders() {
+ return headers;
+ }
+}
diff --git a/src/java/nginx/unit/websocket/WsIOException.java b/src/java/nginx/unit/websocket/WsIOException.java
new file mode 100644
index 00000000..0362dc1d
--- /dev/null
+++ b/src/java/nginx/unit/websocket/WsIOException.java
@@ -0,0 +1,41 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.io.IOException;
+
+import javax.websocket.CloseReason;
+
+/**
+ * Allows the WebSocket implementation to throw an {@link IOException} that
+ * includes a {@link CloseReason} specific to the error that can be passed back
+ * to the client.
+ */
+public class WsIOException extends IOException {
+
+ private static final long serialVersionUID = 1L;
+
+ private final CloseReason closeReason;
+
+ public WsIOException(CloseReason closeReason) {
+ this.closeReason = closeReason;
+ }
+
+ public CloseReason getCloseReason() {
+ return closeReason;
+ }
+}
diff --git a/src/java/nginx/unit/websocket/WsPongMessage.java b/src/java/nginx/unit/websocket/WsPongMessage.java
new file mode 100644
index 00000000..531bcda9
--- /dev/null
+++ b/src/java/nginx/unit/websocket/WsPongMessage.java
@@ -0,0 +1,39 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.PongMessage;
+
+public class WsPongMessage implements PongMessage {
+
+ private final ByteBuffer applicationData;
+
+
+ public WsPongMessage(ByteBuffer applicationData) {
+ byte[] dst = new byte[applicationData.limit()];
+ applicationData.get(dst);
+ this.applicationData = ByteBuffer.wrap(dst);
+ }
+
+
+ @Override
+ public ByteBuffer getApplicationData() {
+ return applicationData;
+ }
+}
diff --git a/src/java/nginx/unit/websocket/WsRemoteEndpointAsync.java b/src/java/nginx/unit/websocket/WsRemoteEndpointAsync.java
new file mode 100644
index 00000000..0ea20795
--- /dev/null
+++ b/src/java/nginx/unit/websocket/WsRemoteEndpointAsync.java
@@ -0,0 +1,79 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.Future;
+
+import javax.websocket.RemoteEndpoint;
+import javax.websocket.SendHandler;
+
+public class WsRemoteEndpointAsync extends WsRemoteEndpointBase
+ implements RemoteEndpoint.Async {
+
+ WsRemoteEndpointAsync(WsRemoteEndpointImplBase base) {
+ super(base);
+ }
+
+
+ @Override
+ public long getSendTimeout() {
+ return base.getSendTimeout();
+ }
+
+
+ @Override
+ public void setSendTimeout(long timeout) {
+ base.setSendTimeout(timeout);
+ }
+
+
+ @Override
+ public void sendText(String text, SendHandler completion) {
+ base.sendStringByCompletion(text, completion);
+ }
+
+
+ @Override
+ public Future<Void> sendText(String text) {
+ return base.sendStringByFuture(text);
+ }
+
+
+ @Override
+ public Future<Void> sendBinary(ByteBuffer data) {
+ return base.sendBytesByFuture(data);
+ }
+
+
+ @Override
+ public void sendBinary(ByteBuffer data, SendHandler completion) {
+ base.sendBytesByCompletion(data, completion);
+ }
+
+
+ @Override
+ public Future<Void> sendObject(Object obj) {
+ return base.sendObjectByFuture(obj);
+ }
+
+
+ @Override
+ public void sendObject(Object obj, SendHandler completion) {
+ base.sendObjectByCompletion(obj, completion);
+ }
+}
diff --git a/src/java/nginx/unit/websocket/WsRemoteEndpointBase.java b/src/java/nginx/unit/websocket/WsRemoteEndpointBase.java
new file mode 100644
index 00000000..21cb2040
--- /dev/null
+++ b/src/java/nginx/unit/websocket/WsRemoteEndpointBase.java
@@ -0,0 +1,64 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import javax.websocket.RemoteEndpoint;
+
+public abstract class WsRemoteEndpointBase implements RemoteEndpoint {
+
+ protected final WsRemoteEndpointImplBase base;
+
+
+ WsRemoteEndpointBase(WsRemoteEndpointImplBase base) {
+ this.base = base;
+ }
+
+
+ @Override
+ public final void setBatchingAllowed(boolean batchingAllowed) throws IOException {
+ base.setBatchingAllowed(batchingAllowed);
+ }
+
+
+ @Override
+ public final boolean getBatchingAllowed() {
+ return base.getBatchingAllowed();
+ }
+
+
+ @Override
+ public final void flushBatch() throws IOException {
+ base.flushBatch();
+ }
+
+
+ @Override
+ public final void sendPing(ByteBuffer applicationData) throws IOException,
+ IllegalArgumentException {
+ base.sendPing(applicationData);
+ }
+
+
+ @Override
+ public final void sendPong(ByteBuffer applicationData) throws IOException,
+ IllegalArgumentException {
+ base.sendPong(applicationData);
+ }
+}
diff --git a/src/java/nginx/unit/websocket/WsRemoteEndpointBasic.java b/src/java/nginx/unit/websocket/WsRemoteEndpointBasic.java
new file mode 100644
index 00000000..2a93cc7b
--- /dev/null
+++ b/src/java/nginx/unit/websocket/WsRemoteEndpointBasic.java
@@ -0,0 +1,76 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.nio.ByteBuffer;
+
+import javax.websocket.EncodeException;
+import javax.websocket.RemoteEndpoint;
+
+public class WsRemoteEndpointBasic extends WsRemoteEndpointBase
+ implements RemoteEndpoint.Basic {
+
+ WsRemoteEndpointBasic(WsRemoteEndpointImplBase base) {
+ super(base);
+ }
+
+
+ @Override
+ public void sendText(String text) throws IOException {
+ base.sendString(text);
+ }
+
+
+ @Override
+ public void sendBinary(ByteBuffer data) throws IOException {
+ base.sendBytes(data);
+ }
+
+
+ @Override
+ public void sendText(String fragment, boolean isLast) throws IOException {
+ base.sendPartialString(fragment, isLast);
+ }
+
+
+ @Override
+ public void sendBinary(ByteBuffer partialByte, boolean isLast)
+ throws IOException {
+ base.sendPartialBytes(partialByte, isLast);
+ }
+
+
+ @Override
+ public OutputStream getSendStream() throws IOException {
+ return base.getSendStream();
+ }
+
+
+ @Override
+ public Writer getSendWriter() throws IOException {
+ return base.getSendWriter();
+ }
+
+
+ @Override
+ public void sendObject(Object o) throws IOException, EncodeException {
+ base.sendObject(o);
+ }
+}
diff --git a/src/java/nginx/unit/websocket/WsRemoteEndpointImplBase.java b/src/java/nginx/unit/websocket/WsRemoteEndpointImplBase.java
new file mode 100644
index 00000000..776124fd
--- /dev/null
+++ b/src/java/nginx/unit/websocket/WsRemoteEndpointImplBase.java
@@ -0,0 +1,1234 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.net.SocketTimeoutException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.Future;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCodes;
+import javax.websocket.DeploymentException;
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+import javax.websocket.RemoteEndpoint;
+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.buf.Utf8Encoder;
+import org.apache.tomcat.util.res.StringManager;
+
+import nginx.unit.Request;
+
+public abstract class WsRemoteEndpointImplBase implements RemoteEndpoint {
+
+ private static final StringManager sm =
+ StringManager.getManager(WsRemoteEndpointImplBase.class);
+
+ protected static final SendResult SENDRESULT_OK = new SendResult();
+
+ private final Log log = LogFactory.getLog(WsRemoteEndpointImplBase.class); // must not be static
+
+ private final StateMachine stateMachine = new StateMachine();
+
+ private final IntermediateMessageHandler intermediateMessageHandler =
+ new IntermediateMessageHandler(this);
+
+ private Transformation transformation = null;
+ private final Semaphore messagePartInProgress = new Semaphore(1);
+ private final Queue<MessagePart> messagePartQueue = new ArrayDeque<>();
+ private final Object messagePartLock = new Object();
+
+ // State
+ private volatile boolean closed = false;
+ private boolean fragmented = false;
+ private boolean nextFragmented = false;
+ private boolean text = false;
+ private boolean nextText = false;
+
+ // Max size of WebSocket header is 14 bytes
+ private final ByteBuffer headerBuffer = ByteBuffer.allocate(14);
+ private final ByteBuffer outputBuffer = ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE);
+ private final CharsetEncoder encoder = new Utf8Encoder();
+ private final ByteBuffer encoderBuffer = ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE);
+ private final AtomicBoolean batchingAllowed = new AtomicBoolean(false);
+ private volatile long sendTimeout = -1;
+ private WsSession wsSession;
+ private List<EncoderEntry> encoderEntries = new ArrayList<>();
+
+ private Request request;
+
+
+ protected void setTransformation(Transformation transformation) {
+ this.transformation = transformation;
+ }
+
+
+ public long getSendTimeout() {
+ return sendTimeout;
+ }
+
+
+ public void setSendTimeout(long timeout) {
+ this.sendTimeout = timeout;
+ }
+
+
+ @Override
+ public void setBatchingAllowed(boolean batchingAllowed) throws IOException {
+ boolean oldValue = this.batchingAllowed.getAndSet(batchingAllowed);
+
+ if (oldValue && !batchingAllowed) {
+ flushBatch();
+ }
+ }
+
+
+ @Override
+ public boolean getBatchingAllowed() {
+ return batchingAllowed.get();
+ }
+
+
+ @Override
+ public void flushBatch() throws IOException {
+ sendMessageBlock(Constants.INTERNAL_OPCODE_FLUSH, null, true);
+ }
+
+
+ public void sendBytes(ByteBuffer data) throws IOException {
+ if (data == null) {
+ throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullData"));
+ }
+ stateMachine.binaryStart();
+ sendMessageBlock(Constants.OPCODE_BINARY, data, true);
+ stateMachine.complete(true);
+ }
+
+
+ public Future<Void> sendBytesByFuture(ByteBuffer data) {
+ FutureToSendHandler f2sh = new FutureToSendHandler(wsSession);
+ sendBytesByCompletion(data, f2sh);
+ return f2sh;
+ }
+
+
+ public void sendBytesByCompletion(ByteBuffer data, SendHandler handler) {
+ if (data == null) {
+ throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullData"));
+ }
+ if (handler == null) {
+ throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullHandler"));
+ }
+ StateUpdateSendHandler sush = new StateUpdateSendHandler(handler, stateMachine);
+ stateMachine.binaryStart();
+ startMessage(Constants.OPCODE_BINARY, data, true, sush);
+ }
+
+
+ public void sendPartialBytes(ByteBuffer partialByte, boolean last)
+ throws IOException {
+ if (partialByte == null) {
+ throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullData"));
+ }
+ stateMachine.binaryPartialStart();
+ sendMessageBlock(Constants.OPCODE_BINARY, partialByte, last);
+ stateMachine.complete(last);
+ }
+
+
+ @Override
+ public void sendPing(ByteBuffer applicationData) throws IOException,
+ IllegalArgumentException {
+ if (applicationData.remaining() > 125) {
+ throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.tooMuchData"));
+ }
+ sendMessageBlock(Constants.OPCODE_PING, applicationData, true);
+ }
+
+
+ @Override
+ public void sendPong(ByteBuffer applicationData) throws IOException,
+ IllegalArgumentException {
+ if (applicationData.remaining() > 125) {
+ throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.tooMuchData"));
+ }
+ sendMessageBlock(Constants.OPCODE_PONG, applicationData, true);
+ }
+
+
+ public void sendString(String text) throws IOException {
+ if (text == null) {
+ throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullData"));
+ }
+ stateMachine.textStart();
+ sendMessageBlock(CharBuffer.wrap(text), true);
+ }
+
+
+ public Future<Void> sendStringByFuture(String text) {
+ FutureToSendHandler f2sh = new FutureToSendHandler(wsSession);
+ sendStringByCompletion(text, f2sh);
+ return f2sh;
+ }
+
+
+ public void sendStringByCompletion(String text, SendHandler handler) {
+ if (text == null) {
+ throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullData"));
+ }
+ if (handler == null) {
+ throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullHandler"));
+ }
+ stateMachine.textStart();
+ TextMessageSendHandler tmsh = new TextMessageSendHandler(handler,
+ CharBuffer.wrap(text), true, encoder, encoderBuffer, this);
+ tmsh.write();
+ // TextMessageSendHandler will update stateMachine when it completes
+ }
+
+
+ public void sendPartialString(String fragment, boolean isLast)
+ throws IOException {
+ if (fragment == null) {
+ throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullData"));
+ }
+ stateMachine.textPartialStart();
+ sendMessageBlock(CharBuffer.wrap(fragment), isLast);
+ }
+
+
+ public OutputStream getSendStream() {
+ stateMachine.streamStart();
+ return new WsOutputStream(this);
+ }
+
+
+ public Writer getSendWriter() {
+ stateMachine.writeStart();
+ return new WsWriter(this);
+ }
+
+
+ void sendMessageBlock(CharBuffer part, boolean last) throws IOException {
+ long timeoutExpiry = getTimeoutExpiry();
+ boolean isDone = false;
+ while (!isDone) {
+ encoderBuffer.clear();
+ CoderResult cr = encoder.encode(part, encoderBuffer, true);
+ if (cr.isError()) {
+ throw new IllegalArgumentException(cr.toString());
+ }
+ isDone = !cr.isOverflow();
+ encoderBuffer.flip();
+ sendMessageBlock(Constants.OPCODE_TEXT, encoderBuffer, last && isDone, timeoutExpiry);
+ }
+ stateMachine.complete(last);
+ }
+
+
+ void sendMessageBlock(byte opCode, ByteBuffer payload, boolean last)
+ throws IOException {
+ sendMessageBlock(opCode, payload, last, getTimeoutExpiry());
+ }
+
+
+ private long getTimeoutExpiry() {
+ // Get the timeout before we send the message. The message may
+ // trigger a session close and depending on timing the client
+ // session may close before we can read the timeout.
+ long timeout = getBlockingSendTimeout();
+ if (timeout < 0) {
+ return Long.MAX_VALUE;
+ } else {
+ return System.currentTimeMillis() + timeout;
+ }
+ }
+
+ private byte currentOpCode = Constants.OPCODE_CONTINUATION;
+
+ private void sendMessageBlock(byte opCode, ByteBuffer payload, boolean last,
+ long timeoutExpiry) throws IOException {
+ wsSession.updateLastActive();
+
+ if (opCode == currentOpCode) {
+ opCode = Constants.OPCODE_CONTINUATION;
+ }
+
+ request.sendWsFrame(payload, opCode, last, timeoutExpiry);
+
+ if (!last && opCode != Constants.OPCODE_CONTINUATION) {
+ currentOpCode = opCode;
+ }
+
+ if (last && opCode == Constants.OPCODE_CONTINUATION) {
+ currentOpCode = Constants.OPCODE_CONTINUATION;
+ }
+ }
+
+
+ void startMessage(byte opCode, ByteBuffer payload, boolean last,
+ SendHandler handler) {
+
+ wsSession.updateLastActive();
+
+ List<MessagePart> messageParts = new ArrayList<>();
+ messageParts.add(new MessagePart(last, 0, opCode, payload,
+ intermediateMessageHandler,
+ new EndMessageHandler(this, handler), -1));
+
+ messageParts = transformation.sendMessagePart(messageParts);
+
+ // Some extensions/transformations may buffer messages so it is possible
+ // that no message parts will be returned. If this is the case the
+ // trigger the supplied SendHandler
+ if (messageParts.size() == 0) {
+ handler.onResult(new SendResult());
+ return;
+ }
+
+ MessagePart mp = messageParts.remove(0);
+
+ boolean doWrite = false;
+ synchronized (messagePartLock) {
+ if (Constants.OPCODE_CLOSE == mp.getOpCode() && getBatchingAllowed()) {
+ // Should not happen. To late to send batched messages now since
+ // the session has been closed. Complain loudly.
+ log.warn(sm.getString("wsRemoteEndpoint.flushOnCloseFailed"));
+ }
+ if (messagePartInProgress.tryAcquire()) {
+ doWrite = true;
+ } else {
+ // When a control message is sent while another message is being
+ // sent, the control message is queued. Chances are the
+ // subsequent data message part will end up queued while the
+ // control message is sent. The logic in this class (state
+ // machine, EndMessageHandler, TextMessageSendHandler) ensures
+ // that there will only ever be one data message part in the
+ // queue. There could be multiple control messages in the queue.
+
+ // Add it to the queue
+ messagePartQueue.add(mp);
+ }
+ // Add any remaining messages to the queue
+ messagePartQueue.addAll(messageParts);
+ }
+ if (doWrite) {
+ // Actual write has to be outside sync block to avoid possible
+ // deadlock between messagePartLock and writeLock in
+ // o.a.coyote.http11.upgrade.AbstractServletOutputStream
+ writeMessagePart(mp);
+ }
+ }
+
+
+ void endMessage(SendHandler handler, SendResult result) {
+ boolean doWrite = false;
+ MessagePart mpNext = null;
+ synchronized (messagePartLock) {
+
+ fragmented = nextFragmented;
+ text = nextText;
+
+ mpNext = messagePartQueue.poll();
+ if (mpNext == null) {
+ messagePartInProgress.release();
+ } else if (!closed){
+ // Session may have been closed unexpectedly in the middle of
+ // sending a fragmented message closing the endpoint. If this
+ // happens, clearly there is no point trying to send the rest of
+ // the message.
+ doWrite = true;
+ }
+ }
+ if (doWrite) {
+ // Actual write has to be outside sync block to avoid possible
+ // deadlock between messagePartLock and writeLock in
+ // o.a.coyote.http11.upgrade.AbstractServletOutputStream
+ writeMessagePart(mpNext);
+ }
+
+ wsSession.updateLastActive();
+
+ // Some handlers, such as the IntermediateMessageHandler, do not have a
+ // nested handler so handler may be null.
+ if (handler != null) {
+ handler.onResult(result);
+ }
+ }
+
+
+ void writeMessagePart(MessagePart mp) {
+ if (closed) {
+ throw new IllegalStateException(
+ sm.getString("wsRemoteEndpoint.closed"));
+ }
+
+ if (Constants.INTERNAL_OPCODE_FLUSH == mp.getOpCode()) {
+ nextFragmented = fragmented;
+ nextText = text;
+ outputBuffer.flip();
+ SendHandler flushHandler = new OutputBufferFlushSendHandler(
+ outputBuffer, mp.getEndHandler());
+ doWrite(flushHandler, mp.getBlockingWriteTimeoutExpiry(), outputBuffer);
+ return;
+ }
+
+ // Control messages may be sent in the middle of fragmented message
+ // so they have no effect on the fragmented or text flags
+ boolean first;
+ if (Util.isControl(mp.getOpCode())) {
+ nextFragmented = fragmented;
+ nextText = text;
+ if (mp.getOpCode() == Constants.OPCODE_CLOSE) {
+ closed = true;
+ }
+ first = true;
+ } else {
+ boolean isText = Util.isText(mp.getOpCode());
+
+ if (fragmented) {
+ // Currently fragmented
+ if (text != isText) {
+ throw new IllegalStateException(
+ sm.getString("wsRemoteEndpoint.changeType"));
+ }
+ nextText = text;
+ nextFragmented = !mp.isFin();
+ first = false;
+ } else {
+ // Wasn't fragmented. Might be now
+ if (mp.isFin()) {
+ nextFragmented = false;
+ } else {
+ nextFragmented = true;
+ nextText = isText;
+ }
+ first = true;
+ }
+ }
+
+ byte[] mask;
+
+ if (isMasked()) {
+ mask = Util.generateMask();
+ } else {
+ mask = null;
+ }
+
+ headerBuffer.clear();
+ writeHeader(headerBuffer, mp.isFin(), mp.getRsv(), mp.getOpCode(),
+ isMasked(), mp.getPayload(), mask, first);
+ headerBuffer.flip();
+
+ if (getBatchingAllowed() || isMasked()) {
+ // Need to write via output buffer
+ OutputBufferSendHandler obsh = new OutputBufferSendHandler(
+ mp.getEndHandler(), mp.getBlockingWriteTimeoutExpiry(),
+ headerBuffer, mp.getPayload(), mask,
+ outputBuffer, !getBatchingAllowed(), this);
+ obsh.write();
+ } else {
+ // Can write directly
+ doWrite(mp.getEndHandler(), mp.getBlockingWriteTimeoutExpiry(),
+ headerBuffer, mp.getPayload());
+ }
+ }
+
+
+ private long getBlockingSendTimeout() {
+ Object obj = wsSession.getUserProperties().get(Constants.BLOCKING_SEND_TIMEOUT_PROPERTY);
+ Long userTimeout = null;
+ if (obj instanceof Long) {
+ userTimeout = (Long) obj;
+ }
+ if (userTimeout == null) {
+ return Constants.DEFAULT_BLOCKING_SEND_TIMEOUT;
+ } else {
+ return userTimeout.longValue();
+ }
+ }
+
+
+ /**
+ * Wraps the user provided handler so that the end point is notified when
+ * the message is complete.
+ */
+ private static class EndMessageHandler implements SendHandler {
+
+ private final WsRemoteEndpointImplBase endpoint;
+ private final SendHandler handler;
+
+ public EndMessageHandler(WsRemoteEndpointImplBase endpoint,
+ SendHandler handler) {
+ this.endpoint = endpoint;
+ this.handler = handler;
+ }
+
+
+ @Override
+ public void onResult(SendResult result) {
+ endpoint.endMessage(handler, result);
+ }
+ }
+
+
+ /**
+ * If a transformation needs to split a {@link MessagePart} into multiple
+ * {@link MessagePart}s, it uses this handler as the end handler for each of
+ * the additional {@link MessagePart}s. This handler notifies this this
+ * class that the {@link MessagePart} has been processed and that the next
+ * {@link MessagePart} in the queue should be started. The final
+ * {@link MessagePart} will use the {@link EndMessageHandler} provided with
+ * the original {@link MessagePart}.
+ */
+ private static class IntermediateMessageHandler implements SendHandler {
+
+ private final WsRemoteEndpointImplBase endpoint;
+
+ public IntermediateMessageHandler(WsRemoteEndpointImplBase endpoint) {
+ this.endpoint = endpoint;
+ }
+
+
+ @Override
+ public void onResult(SendResult result) {
+ endpoint.endMessage(null, result);
+ }
+ }
+
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public void sendObject(Object obj) throws IOException, EncodeException {
+ if (obj == null) {
+ throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullData"));
+ }
+ /*
+ * Note that the implementation will convert primitives and their object
+ * equivalents by default but that users are free to specify their own
+ * encoders and decoders for this if they wish.
+ */
+ Encoder encoder = findEncoder(obj);
+ if (encoder == null && Util.isPrimitive(obj.getClass())) {
+ String msg = obj.toString();
+ sendString(msg);
+ return;
+ }
+ if (encoder == null && byte[].class.isAssignableFrom(obj.getClass())) {
+ ByteBuffer msg = ByteBuffer.wrap((byte[]) obj);
+ sendBytes(msg);
+ return;
+ }
+
+ if (encoder instanceof Encoder.Text) {
+ String msg = ((Encoder.Text) encoder).encode(obj);
+ sendString(msg);
+ } else if (encoder instanceof Encoder.TextStream) {
+ try (Writer w = getSendWriter()) {
+ ((Encoder.TextStream) encoder).encode(obj, w);
+ }
+ } else if (encoder instanceof Encoder.Binary) {
+ ByteBuffer msg = ((Encoder.Binary) encoder).encode(obj);
+ sendBytes(msg);
+ } else if (encoder instanceof Encoder.BinaryStream) {
+ try (OutputStream os = getSendStream()) {
+ ((Encoder.BinaryStream) encoder).encode(obj, os);
+ }
+ } else {
+ throw new EncodeException(obj, sm.getString(
+ "wsRemoteEndpoint.noEncoder", obj.getClass()));
+ }
+ }
+
+
+ public Future<Void> sendObjectByFuture(Object obj) {
+ FutureToSendHandler f2sh = new FutureToSendHandler(wsSession);
+ sendObjectByCompletion(obj, f2sh);
+ return f2sh;
+ }
+
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public void sendObjectByCompletion(Object obj, SendHandler completion) {
+
+ if (obj == null) {
+ throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullData"));
+ }
+ if (completion == null) {
+ throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullHandler"));
+ }
+
+ /*
+ * Note that the implementation will convert primitives and their object
+ * equivalents by default but that users are free to specify their own
+ * encoders and decoders for this if they wish.
+ */
+ Encoder encoder = findEncoder(obj);
+ if (encoder == null && Util.isPrimitive(obj.getClass())) {
+ String msg = obj.toString();
+ sendStringByCompletion(msg, completion);
+ return;
+ }
+ if (encoder == null && byte[].class.isAssignableFrom(obj.getClass())) {
+ ByteBuffer msg = ByteBuffer.wrap((byte[]) obj);
+ sendBytesByCompletion(msg, completion);
+ return;
+ }
+
+ try {
+ if (encoder instanceof Encoder.Text) {
+ String msg = ((Encoder.Text) encoder).encode(obj);
+ sendStringByCompletion(msg, completion);
+ } else if (encoder instanceof Encoder.TextStream) {
+ try (Writer w = getSendWriter()) {
+ ((Encoder.TextStream) encoder).encode(obj, w);
+ }
+ completion.onResult(new SendResult());
+ } else if (encoder instanceof Encoder.Binary) {
+ ByteBuffer msg = ((Encoder.Binary) encoder).encode(obj);
+ sendBytesByCompletion(msg, completion);
+ } else if (encoder instanceof Encoder.BinaryStream) {
+ try (OutputStream os = getSendStream()) {
+ ((Encoder.BinaryStream) encoder).encode(obj, os);
+ }
+ completion.onResult(new SendResult());
+ } else {
+ throw new EncodeException(obj, sm.getString(
+ "wsRemoteEndpoint.noEncoder", obj.getClass()));
+ }
+ } catch (Exception e) {
+ SendResult sr = new SendResult(e);
+ completion.onResult(sr);
+ }
+ }
+
+
+ protected void setSession(WsSession wsSession) {
+ this.wsSession = wsSession;
+ }
+
+
+ protected void setRequest(Request request) {
+ this.request = request;
+ }
+
+ protected void setEncoders(EndpointConfig endpointConfig)
+ throws DeploymentException {
+ encoderEntries.clear();
+ for (Class<? extends Encoder> encoderClazz :
+ endpointConfig.getEncoders()) {
+ Encoder instance;
+ try {
+ instance = encoderClazz.getConstructor().newInstance();
+ instance.init(endpointConfig);
+ } catch (ReflectiveOperationException e) {
+ throw new DeploymentException(
+ sm.getString("wsRemoteEndpoint.invalidEncoder",
+ encoderClazz.getName()), e);
+ }
+ EncoderEntry entry = new EncoderEntry(
+ Util.getEncoderType(encoderClazz), instance);
+ encoderEntries.add(entry);
+ }
+ }
+
+
+ private Encoder findEncoder(Object obj) {
+ for (EncoderEntry entry : encoderEntries) {
+ if (entry.getClazz().isAssignableFrom(obj.getClass())) {
+ return entry.getEncoder();
+ }
+ }
+ return null;
+ }
+
+
+ public final void close() {
+ for (EncoderEntry entry : encoderEntries) {
+ entry.getEncoder().destroy();
+ }
+
+ request.closeWs();
+ }
+
+
+ protected abstract void doWrite(SendHandler handler, long blockingWriteTimeoutExpiry,
+ ByteBuffer... data);
+ protected abstract boolean isMasked();
+ protected abstract void doClose();
+
+ private static void writeHeader(ByteBuffer headerBuffer, boolean fin,
+ int rsv, byte opCode, boolean masked, ByteBuffer payload,
+ byte[] mask, boolean first) {
+
+ byte b = 0;
+
+ if (fin) {
+ // Set the fin bit
+ b -= 128;
+ }
+
+ b += (rsv << 4);
+
+ if (first) {
+ // This is the first fragment of this message
+ b += opCode;
+ }
+ // If not the first fragment, it is a continuation with opCode of zero
+
+ headerBuffer.put(b);
+
+ if (masked) {
+ b = (byte) 0x80;
+ } else {
+ b = 0;
+ }
+
+ // Next write the mask && length length
+ if (payload.limit() < 126) {
+ headerBuffer.put((byte) (payload.limit() | b));
+ } else if (payload.limit() < 65536) {
+ headerBuffer.put((byte) (126 | b));
+ headerBuffer.put((byte) (payload.limit() >>> 8));
+ headerBuffer.put((byte) (payload.limit() & 0xFF));
+ } else {
+ // Will never be more than 2^31-1
+ headerBuffer.put((byte) (127 | b));
+ headerBuffer.put((byte) 0);
+ headerBuffer.put((byte) 0);
+ headerBuffer.put((byte) 0);
+ headerBuffer.put((byte) 0);
+ headerBuffer.put((byte) (payload.limit() >>> 24));
+ headerBuffer.put((byte) (payload.limit() >>> 16));
+ headerBuffer.put((byte) (payload.limit() >>> 8));
+ headerBuffer.put((byte) (payload.limit() & 0xFF));
+ }
+ if (masked) {
+ headerBuffer.put(mask[0]);
+ headerBuffer.put(mask[1]);
+ headerBuffer.put(mask[2]);
+ headerBuffer.put(mask[3]);
+ }
+ }
+
+
+ private class TextMessageSendHandler implements SendHandler {
+
+ private final SendHandler handler;
+ private final CharBuffer message;
+ private final boolean isLast;
+ private final CharsetEncoder encoder;
+ private final ByteBuffer buffer;
+ private final WsRemoteEndpointImplBase endpoint;
+ private volatile boolean isDone = false;
+
+ public TextMessageSendHandler(SendHandler handler, CharBuffer message,
+ boolean isLast, CharsetEncoder encoder,
+ ByteBuffer encoderBuffer, WsRemoteEndpointImplBase endpoint) {
+ this.handler = handler;
+ this.message = message;
+ this.isLast = isLast;
+ this.encoder = encoder.reset();
+ this.buffer = encoderBuffer;
+ this.endpoint = endpoint;
+ }
+
+ public void write() {
+ buffer.clear();
+ CoderResult cr = encoder.encode(message, buffer, true);
+ if (cr.isError()) {
+ throw new IllegalArgumentException(cr.toString());
+ }
+ isDone = !cr.isOverflow();
+ buffer.flip();
+ endpoint.startMessage(Constants.OPCODE_TEXT, buffer,
+ isDone && isLast, this);
+ }
+
+ @Override
+ public void onResult(SendResult result) {
+ if (isDone) {
+ endpoint.stateMachine.complete(isLast);
+ handler.onResult(result);
+ } else if(!result.isOK()) {
+ handler.onResult(result);
+ } else if (closed){
+ SendResult sr = new SendResult(new IOException(
+ sm.getString("wsRemoteEndpoint.closedDuringMessage")));
+ handler.onResult(sr);
+ } else {
+ write();
+ }
+ }
+ }
+
+
+ /**
+ * Used to write data to the output buffer, flushing the buffer if it fills
+ * up.
+ */
+ private static class OutputBufferSendHandler implements SendHandler {
+
+ private final SendHandler handler;
+ private final long blockingWriteTimeoutExpiry;
+ private final ByteBuffer headerBuffer;
+ private final ByteBuffer payload;
+ private final byte[] mask;
+ private final ByteBuffer outputBuffer;
+ private final boolean flushRequired;
+ private final WsRemoteEndpointImplBase endpoint;
+ private int maskIndex = 0;
+
+ public OutputBufferSendHandler(SendHandler completion,
+ long blockingWriteTimeoutExpiry,
+ ByteBuffer headerBuffer, ByteBuffer payload, byte[] mask,
+ ByteBuffer outputBuffer, boolean flushRequired,
+ WsRemoteEndpointImplBase endpoint) {
+ this.blockingWriteTimeoutExpiry = blockingWriteTimeoutExpiry;
+ this.handler = completion;
+ this.headerBuffer = headerBuffer;
+ this.payload = payload;
+ this.mask = mask;
+ this.outputBuffer = outputBuffer;
+ this.flushRequired = flushRequired;
+ this.endpoint = endpoint;
+ }
+
+ public void write() {
+ // Write the header
+ while (headerBuffer.hasRemaining() && outputBuffer.hasRemaining()) {
+ outputBuffer.put(headerBuffer.get());
+ }
+ if (headerBuffer.hasRemaining()) {
+ // Still more headers to write, need to flush
+ outputBuffer.flip();
+ endpoint.doWrite(this, blockingWriteTimeoutExpiry, outputBuffer);
+ return;
+ }
+
+ // Write the payload
+ int payloadLeft = payload.remaining();
+ int payloadLimit = payload.limit();
+ int outputSpace = outputBuffer.remaining();
+ int toWrite = payloadLeft;
+
+ if (payloadLeft > outputSpace) {
+ toWrite = outputSpace;
+ // Temporarily reduce the limit
+ payload.limit(payload.position() + toWrite);
+ }
+
+ if (mask == null) {
+ // Use a bulk copy
+ outputBuffer.put(payload);
+ } else {
+ for (int i = 0; i < toWrite; i++) {
+ outputBuffer.put(
+ (byte) (payload.get() ^ (mask[maskIndex++] & 0xFF)));
+ if (maskIndex > 3) {
+ maskIndex = 0;
+ }
+ }
+ }
+
+ if (payloadLeft > outputSpace) {
+ // Restore the original limit
+ payload.limit(payloadLimit);
+ // Still more data to write, need to flush
+ outputBuffer.flip();
+ endpoint.doWrite(this, blockingWriteTimeoutExpiry, outputBuffer);
+ return;
+ }
+
+ if (flushRequired) {
+ outputBuffer.flip();
+ if (outputBuffer.remaining() == 0) {
+ handler.onResult(SENDRESULT_OK);
+ } else {
+ endpoint.doWrite(this, blockingWriteTimeoutExpiry, outputBuffer);
+ }
+ } else {
+ handler.onResult(SENDRESULT_OK);
+ }
+ }
+
+ // ------------------------------------------------- SendHandler methods
+ @Override
+ public void onResult(SendResult result) {
+ if (result.isOK()) {
+ if (outputBuffer.hasRemaining()) {
+ endpoint.doWrite(this, blockingWriteTimeoutExpiry, outputBuffer);
+ } else {
+ outputBuffer.clear();
+ write();
+ }
+ } else {
+ handler.onResult(result);
+ }
+ }
+ }
+
+
+ /**
+ * Ensures that the output buffer is cleared after it has been flushed.
+ */
+ private static class OutputBufferFlushSendHandler implements SendHandler {
+
+ private final ByteBuffer outputBuffer;
+ private final SendHandler handler;
+
+ public OutputBufferFlushSendHandler(ByteBuffer outputBuffer, SendHandler handler) {
+ this.outputBuffer = outputBuffer;
+ this.handler = handler;
+ }
+
+ @Override
+ public void onResult(SendResult result) {
+ if (result.isOK()) {
+ outputBuffer.clear();
+ }
+ handler.onResult(result);
+ }
+ }
+
+
+ private static class WsOutputStream extends OutputStream {
+
+ private final WsRemoteEndpointImplBase endpoint;
+ private final ByteBuffer buffer = ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE);
+ private final Object closeLock = new Object();
+ private volatile boolean closed = false;
+ private volatile boolean used = false;
+
+ public WsOutputStream(WsRemoteEndpointImplBase endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ if (closed) {
+ throw new IllegalStateException(
+ sm.getString("wsRemoteEndpoint.closedOutputStream"));
+ }
+
+ used = true;
+ if (buffer.remaining() == 0) {
+ flush();
+ }
+ buffer.put((byte) b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ if (closed) {
+ throw new IllegalStateException(
+ sm.getString("wsRemoteEndpoint.closedOutputStream"));
+ }
+ if (len == 0) {
+ return;
+ }
+ if ((off < 0) || (off > b.length) || (len < 0) ||
+ ((off + len) > b.length) || ((off + len) < 0)) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ used = true;
+ if (buffer.remaining() == 0) {
+ flush();
+ }
+ int remaining = buffer.remaining();
+ int written = 0;
+
+ while (remaining < len - written) {
+ buffer.put(b, off + written, remaining);
+ written += remaining;
+ flush();
+ remaining = buffer.remaining();
+ }
+ buffer.put(b, off + written, len - written);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ if (closed) {
+ throw new IllegalStateException(
+ sm.getString("wsRemoteEndpoint.closedOutputStream"));
+ }
+
+ // Optimisation. If there is no data to flush then do not send an
+ // empty message.
+ if (!Constants.STREAMS_DROP_EMPTY_MESSAGES || buffer.position() > 0) {
+ doWrite(false);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ synchronized (closeLock) {
+ if (closed) {
+ return;
+ }
+ closed = true;
+ }
+
+ doWrite(true);
+ }
+
+ private void doWrite(boolean last) throws IOException {
+ if (!Constants.STREAMS_DROP_EMPTY_MESSAGES || used) {
+ buffer.flip();
+ endpoint.sendMessageBlock(Constants.OPCODE_BINARY, buffer, last);
+ }
+ endpoint.stateMachine.complete(last);
+ buffer.clear();
+ }
+ }
+
+
+ private static class WsWriter extends Writer {
+
+ private final WsRemoteEndpointImplBase endpoint;
+ private final CharBuffer buffer = CharBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE);
+ private final Object closeLock = new Object();
+ private volatile boolean closed = false;
+ private volatile boolean used = false;
+
+ public WsWriter(WsRemoteEndpointImplBase endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ @Override
+ public void write(char[] cbuf, int off, int len) throws IOException {
+ if (closed) {
+ throw new IllegalStateException(
+ sm.getString("wsRemoteEndpoint.closedWriter"));
+ }
+ if (len == 0) {
+ return;
+ }
+ if ((off < 0) || (off > cbuf.length) || (len < 0) ||
+ ((off + len) > cbuf.length) || ((off + len) < 0)) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ used = true;
+ if (buffer.remaining() == 0) {
+ flush();
+ }
+ int remaining = buffer.remaining();
+ int written = 0;
+
+ while (remaining < len - written) {
+ buffer.put(cbuf, off + written, remaining);
+ written += remaining;
+ flush();
+ remaining = buffer.remaining();
+ }
+ buffer.put(cbuf, off + written, len - written);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ if (closed) {
+ throw new IllegalStateException(
+ sm.getString("wsRemoteEndpoint.closedWriter"));
+ }
+
+ if (!Constants.STREAMS_DROP_EMPTY_MESSAGES || buffer.position() > 0) {
+ doWrite(false);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ synchronized (closeLock) {
+ if (closed) {
+ return;
+ }
+ closed = true;
+ }
+
+ doWrite(true);
+ }
+
+ private void doWrite(boolean last) throws IOException {
+ if (!Constants.STREAMS_DROP_EMPTY_MESSAGES || used) {
+ buffer.flip();
+ endpoint.sendMessageBlock(buffer, last);
+ buffer.clear();
+ } else {
+ endpoint.stateMachine.complete(last);
+ }
+ }
+ }
+
+
+ private static class EncoderEntry {
+
+ private final Class<?> clazz;
+ private final Encoder encoder;
+
+ public EncoderEntry(Class<?> clazz, Encoder encoder) {
+ this.clazz = clazz;
+ this.encoder = encoder;
+ }
+
+ public Class<?> getClazz() {
+ return clazz;
+ }
+
+ public Encoder getEncoder() {
+ return encoder;
+ }
+ }
+
+
+ private enum State {
+ OPEN,
+ STREAM_WRITING,
+ WRITER_WRITING,
+ BINARY_PARTIAL_WRITING,
+ BINARY_PARTIAL_READY,
+ BINARY_FULL_WRITING,
+ TEXT_PARTIAL_WRITING,
+ TEXT_PARTIAL_READY,
+ TEXT_FULL_WRITING
+ }
+
+
+ private static class StateMachine {
+ private State state = State.OPEN;
+
+ public synchronized void streamStart() {
+ checkState(State.OPEN);
+ state = State.STREAM_WRITING;
+ }
+
+ public synchronized void writeStart() {
+ checkState(State.OPEN);
+ state = State.WRITER_WRITING;
+ }
+
+ public synchronized void binaryPartialStart() {
+ checkState(State.OPEN, State.BINARY_PARTIAL_READY);
+ state = State.BINARY_PARTIAL_WRITING;
+ }
+
+ public synchronized void binaryStart() {
+ checkState(State.OPEN);
+ state = State.BINARY_FULL_WRITING;
+ }
+
+ public synchronized void textPartialStart() {
+ checkState(State.OPEN, State.TEXT_PARTIAL_READY);
+ state = State.TEXT_PARTIAL_WRITING;
+ }
+
+ public synchronized void textStart() {
+ checkState(State.OPEN);
+ state = State.TEXT_FULL_WRITING;
+ }
+
+ public synchronized void complete(boolean last) {
+ if (last) {
+ checkState(State.TEXT_PARTIAL_WRITING, State.TEXT_FULL_WRITING,
+ State.BINARY_PARTIAL_WRITING, State.BINARY_FULL_WRITING,
+ State.STREAM_WRITING, State.WRITER_WRITING);
+ state = State.OPEN;
+ } else {
+ checkState(State.TEXT_PARTIAL_WRITING, State.BINARY_PARTIAL_WRITING,
+ State.STREAM_WRITING, State.WRITER_WRITING);
+ if (state == State.TEXT_PARTIAL_WRITING) {
+ state = State.TEXT_PARTIAL_READY;
+ } else if (state == State.BINARY_PARTIAL_WRITING){
+ state = State.BINARY_PARTIAL_READY;
+ } else if (state == State.WRITER_WRITING) {
+ // NO-OP. Leave state as is.
+ } else if (state == State.STREAM_WRITING) {
+ // NO-OP. Leave state as is.
+ } else {
+ // Should never happen
+ // The if ... else ... blocks above should cover all states
+ // permitted by the preceding checkState() call
+ throw new IllegalStateException(
+ "BUG: This code should never be called");
+ }
+ }
+ }
+
+ private void checkState(State... required) {
+ for (State state : required) {
+ if (this.state == state) {
+ return;
+ }
+ }
+ throw new IllegalStateException(
+ sm.getString("wsRemoteEndpoint.wrongState", this.state));
+ }
+ }
+
+
+ private static class StateUpdateSendHandler implements SendHandler {
+
+ private final SendHandler handler;
+ private final StateMachine stateMachine;
+
+ public StateUpdateSendHandler(SendHandler handler, StateMachine stateMachine) {
+ this.handler = handler;
+ this.stateMachine = stateMachine;
+ }
+
+ @Override
+ public void onResult(SendResult result) {
+ if (result.isOK()) {
+ stateMachine.complete(true);
+ }
+ handler.onResult(result);
+ }
+ }
+
+
+ private static class BlockingSendHandler implements SendHandler {
+
+ private SendResult sendResult = null;
+
+ @Override
+ public void onResult(SendResult result) {
+ sendResult = result;
+ }
+
+ public SendResult getSendResult() {
+ return sendResult;
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/WsRemoteEndpointImplClient.java b/src/java/nginx/unit/websocket/WsRemoteEndpointImplClient.java
new file mode 100644
index 00000000..70b66789
--- /dev/null
+++ b/src/java/nginx/unit/websocket/WsRemoteEndpointImplClient.java
@@ -0,0 +1,75 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.websocket.SendHandler;
+import javax.websocket.SendResult;
+
+public class WsRemoteEndpointImplClient extends WsRemoteEndpointImplBase {
+
+ private final AsyncChannelWrapper channel;
+
+ public WsRemoteEndpointImplClient(AsyncChannelWrapper channel) {
+ this.channel = channel;
+ }
+
+
+ @Override
+ protected boolean isMasked() {
+ return true;
+ }
+
+
+ @Override
+ protected void doWrite(SendHandler handler, long blockingWriteTimeoutExpiry,
+ ByteBuffer... data) {
+ long timeout;
+ for (ByteBuffer byteBuffer : data) {
+ if (blockingWriteTimeoutExpiry == -1) {
+ timeout = getSendTimeout();
+ if (timeout < 1) {
+ timeout = Long.MAX_VALUE;
+ }
+ } else {
+ timeout = blockingWriteTimeoutExpiry - System.currentTimeMillis();
+ if (timeout < 0) {
+ SendResult sr = new SendResult(new IOException("Blocking write timeout"));
+ handler.onResult(sr);
+ }
+ }
+
+ try {
+ channel.write(byteBuffer).get(timeout, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ handler.onResult(new SendResult(e));
+ return;
+ }
+ }
+ handler.onResult(SENDRESULT_OK);
+ }
+
+ @Override
+ protected void doClose() {
+ channel.close();
+ }
+}
diff --git a/src/java/nginx/unit/websocket/WsSession.java b/src/java/nginx/unit/websocket/WsSession.java
new file mode 100644
index 00000000..b654eb37
--- /dev/null
+++ b/src/java/nginx/unit/websocket/WsSession.java
@@ -0,0 +1,1070 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.CharBuffer;
+import java.nio.channels.WritePendingException;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCode;
+import javax.websocket.CloseReason.CloseCodes;
+import javax.websocket.DeploymentException;
+import javax.websocket.Endpoint;
+import javax.websocket.EndpointConfig;
+import javax.websocket.Extension;
+import javax.websocket.MessageHandler;
+import javax.websocket.MessageHandler.Partial;
+import javax.websocket.MessageHandler.Whole;
+import javax.websocket.PongMessage;
+import javax.websocket.RemoteEndpoint;
+import javax.websocket.SendResult;
+import javax.websocket.Session;
+import javax.websocket.WebSocketContainer;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.InstanceManager;
+import org.apache.tomcat.InstanceManagerBindings;
+import org.apache.tomcat.util.ExceptionUtils;
+import org.apache.tomcat.util.buf.Utf8Decoder;
+import org.apache.tomcat.util.res.StringManager;
+
+import nginx.unit.Request;
+
+public class WsSession implements Session {
+
+ // An ellipsis is a single character that looks like three periods in a row
+ // and is used to indicate a continuation.
+ private static final byte[] ELLIPSIS_BYTES = "\u2026".getBytes(StandardCharsets.UTF_8);
+ // An ellipsis is three bytes in UTF-8
+ private static final int ELLIPSIS_BYTES_LEN = ELLIPSIS_BYTES.length;
+
+ private static final StringManager sm = StringManager.getManager(WsSession.class);
+ private static AtomicLong ids = new AtomicLong(0);
+
+ private final Log log = LogFactory.getLog(WsSession.class); // must not be static
+
+ private final CharsetDecoder utf8DecoderMessage = new Utf8Decoder().
+ onMalformedInput(CodingErrorAction.REPORT).
+ onUnmappableCharacter(CodingErrorAction.REPORT);
+
+ private final Endpoint localEndpoint;
+ private final WsRemoteEndpointImplBase wsRemoteEndpoint;
+ private final RemoteEndpoint.Async remoteEndpointAsync;
+ private final RemoteEndpoint.Basic remoteEndpointBasic;
+ private final ClassLoader applicationClassLoader;
+ private final WsWebSocketContainer webSocketContainer;
+ private final URI requestUri;
+ private final Map<String, List<String>> requestParameterMap;
+ private final String queryString;
+ private final Principal userPrincipal;
+ private final EndpointConfig endpointConfig;
+
+ private final List<Extension> negotiatedExtensions;
+ private final String subProtocol;
+ private final Map<String, String> pathParameters;
+ private final boolean secure;
+ private final String httpSessionId;
+ private final String id;
+
+ // Expected to handle message types of <String> only
+ private volatile MessageHandler textMessageHandler = null;
+ // Expected to handle message types of <ByteBuffer> only
+ private volatile MessageHandler binaryMessageHandler = null;
+ private volatile MessageHandler.Whole<PongMessage> pongMessageHandler = null;
+ private volatile State state = State.OPEN;
+ private final Object stateLock = new Object();
+ private final Map<String, Object> userProperties = new ConcurrentHashMap<>();
+ private volatile int maxBinaryMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;
+ private volatile int maxTextMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;
+ private volatile long maxIdleTimeout = 0;
+ private volatile long lastActive = System.currentTimeMillis();
+ private Map<FutureToSendHandler, FutureToSendHandler> futures = new ConcurrentHashMap<>();
+
+ private CharBuffer messageBufferText;
+ private ByteBuffer binaryBuffer;
+ private byte startOpCode = Constants.OPCODE_CONTINUATION;
+
+ /**
+ * Creates a new WebSocket session for communication between the two
+ * provided end points. The result of {@link Thread#getContextClassLoader()}
+ * at the time this constructor is called will be used when calling
+ * {@link Endpoint#onClose(Session, CloseReason)}.
+ *
+ * @param localEndpoint The end point managed by this code
+ * @param wsRemoteEndpoint The other / remote endpoint
+ * @param wsWebSocketContainer The container that created this session
+ * @param requestUri The URI used to connect to this endpoint or
+ * <code>null</code> is this is a client session
+ * @param requestParameterMap The parameters associated with the request
+ * that initiated this session or
+ * <code>null</code> if this is a client session
+ * @param queryString The query string associated with the request
+ * that initiated this session or
+ * <code>null</code> if this is a client session
+ * @param userPrincipal The principal associated with the request
+ * that initiated this session or
+ * <code>null</code> if this is a client session
+ * @param httpSessionId The HTTP session ID associated with the
+ * request that initiated this session or
+ * <code>null</code> if this is a client session
+ * @param negotiatedExtensions The agreed extensions to use for this session
+ * @param subProtocol The agreed subprotocol to use for this
+ * session
+ * @param pathParameters The path parameters associated with the
+ * request that initiated this session or
+ * <code>null</code> if this is a client session
+ * @param secure Was this session initiated over a secure
+ * connection?
+ * @param endpointConfig The configuration information for the
+ * endpoint
+ * @throws DeploymentException if an invalid encode is specified
+ */
+ public WsSession(Endpoint localEndpoint,
+ WsRemoteEndpointImplBase wsRemoteEndpoint,
+ WsWebSocketContainer wsWebSocketContainer,
+ URI requestUri, Map<String, List<String>> requestParameterMap,
+ String queryString, Principal userPrincipal, String httpSessionId,
+ List<Extension> negotiatedExtensions, String subProtocol, Map<String, String> pathParameters,
+ boolean secure, EndpointConfig endpointConfig,
+ Request request) throws DeploymentException {
+ this.localEndpoint = localEndpoint;
+ this.wsRemoteEndpoint = wsRemoteEndpoint;
+ this.wsRemoteEndpoint.setSession(this);
+ this.wsRemoteEndpoint.setRequest(request);
+
+ request.setWsSession(this);
+
+ this.remoteEndpointAsync = new WsRemoteEndpointAsync(wsRemoteEndpoint);
+ this.remoteEndpointBasic = new WsRemoteEndpointBasic(wsRemoteEndpoint);
+ this.webSocketContainer = wsWebSocketContainer;
+ applicationClassLoader = Thread.currentThread().getContextClassLoader();
+ wsRemoteEndpoint.setSendTimeout(wsWebSocketContainer.getDefaultAsyncSendTimeout());
+ this.maxBinaryMessageBufferSize = webSocketContainer.getDefaultMaxBinaryMessageBufferSize();
+ this.maxTextMessageBufferSize = webSocketContainer.getDefaultMaxTextMessageBufferSize();
+ this.maxIdleTimeout = webSocketContainer.getDefaultMaxSessionIdleTimeout();
+ this.requestUri = requestUri;
+ if (requestParameterMap == null) {
+ this.requestParameterMap = Collections.emptyMap();
+ } else {
+ this.requestParameterMap = requestParameterMap;
+ }
+ this.queryString = queryString;
+ this.userPrincipal = userPrincipal;
+ this.httpSessionId = httpSessionId;
+ this.negotiatedExtensions = negotiatedExtensions;
+ if (subProtocol == null) {
+ this.subProtocol = "";
+ } else {
+ this.subProtocol = subProtocol;
+ }
+ this.pathParameters = pathParameters;
+ this.secure = secure;
+ this.wsRemoteEndpoint.setEncoders(endpointConfig);
+ this.endpointConfig = endpointConfig;
+
+ this.userProperties.putAll(endpointConfig.getUserProperties());
+ this.id = Long.toHexString(ids.getAndIncrement());
+
+ InstanceManager instanceManager = webSocketContainer.getInstanceManager();
+ if (instanceManager == null) {
+ instanceManager = InstanceManagerBindings.get(applicationClassLoader);
+ }
+ if (instanceManager != null) {
+ try {
+ instanceManager.newInstance(localEndpoint);
+ } catch (Exception e) {
+ throw new DeploymentException(sm.getString("wsSession.instanceNew"), e);
+ }
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("wsSession.created", id));
+ }
+
+ messageBufferText = CharBuffer.allocate(maxTextMessageBufferSize);
+ }
+
+ public static String wsSession_test() {
+ return sm.getString("wsSession.instanceNew");
+ }
+
+
+ @Override
+ public WebSocketContainer getContainer() {
+ checkState();
+ return webSocketContainer;
+ }
+
+
+ @Override
+ public void addMessageHandler(MessageHandler listener) {
+ Class<?> target = Util.getMessageType(listener);
+ doAddMessageHandler(target, listener);
+ }
+
+
+ @Override
+ public <T> void addMessageHandler(Class<T> clazz, Partial<T> handler)
+ throws IllegalStateException {
+ doAddMessageHandler(clazz, handler);
+ }
+
+
+ @Override
+ public <T> void addMessageHandler(Class<T> clazz, Whole<T> handler)
+ throws IllegalStateException {
+ doAddMessageHandler(clazz, handler);
+ }
+
+
+ @SuppressWarnings("unchecked")
+ private void doAddMessageHandler(Class<?> target, MessageHandler listener) {
+ checkState();
+
+ // Message handlers that require decoders may map to text messages,
+ // binary messages, both or neither.
+
+ // The frame processing code expects binary message handlers to
+ // accept ByteBuffer
+
+ // Use the POJO message handler wrappers as they are designed to wrap
+ // arbitrary objects with MessageHandlers and can wrap MessageHandlers
+ // just as easily.
+
+ Set<MessageHandlerResult> mhResults = Util.getMessageHandlers(target, listener,
+ endpointConfig, this);
+
+ for (MessageHandlerResult mhResult : mhResults) {
+ switch (mhResult.getType()) {
+ case TEXT: {
+ if (textMessageHandler != null) {
+ throw new IllegalStateException(sm.getString("wsSession.duplicateHandlerText"));
+ }
+ textMessageHandler = mhResult.getHandler();
+ break;
+ }
+ case BINARY: {
+ if (binaryMessageHandler != null) {
+ throw new IllegalStateException(
+ sm.getString("wsSession.duplicateHandlerBinary"));
+ }
+ binaryMessageHandler = mhResult.getHandler();
+ break;
+ }
+ case PONG: {
+ if (pongMessageHandler != null) {
+ throw new IllegalStateException(sm.getString("wsSession.duplicateHandlerPong"));
+ }
+ MessageHandler handler = mhResult.getHandler();
+ if (handler instanceof MessageHandler.Whole<?>) {
+ pongMessageHandler = (MessageHandler.Whole<PongMessage>) handler;
+ } else {
+ throw new IllegalStateException(
+ sm.getString("wsSession.invalidHandlerTypePong"));
+ }
+
+ break;
+ }
+ default: {
+ throw new IllegalArgumentException(
+ sm.getString("wsSession.unknownHandlerType", listener, mhResult.getType()));
+ }
+ }
+ }
+ }
+
+
+ @Override
+ public Set<MessageHandler> getMessageHandlers() {
+ checkState();
+ Set<MessageHandler> result = new HashSet<>();
+ if (binaryMessageHandler != null) {
+ result.add(binaryMessageHandler);
+ }
+ if (textMessageHandler != null) {
+ result.add(textMessageHandler);
+ }
+ if (pongMessageHandler != null) {
+ result.add(pongMessageHandler);
+ }
+ return result;
+ }
+
+
+ @Override
+ public void removeMessageHandler(MessageHandler listener) {
+ checkState();
+ if (listener == null) {
+ return;
+ }
+
+ MessageHandler wrapped = null;
+
+ if (listener instanceof WrappedMessageHandler) {
+ wrapped = ((WrappedMessageHandler) listener).getWrappedHandler();
+ }
+
+ if (wrapped == null) {
+ wrapped = listener;
+ }
+
+ boolean removed = false;
+ if (wrapped.equals(textMessageHandler) || listener.equals(textMessageHandler)) {
+ textMessageHandler = null;
+ removed = true;
+ }
+
+ if (wrapped.equals(binaryMessageHandler) || listener.equals(binaryMessageHandler)) {
+ binaryMessageHandler = null;
+ removed = true;
+ }
+
+ if (wrapped.equals(pongMessageHandler) || listener.equals(pongMessageHandler)) {
+ pongMessageHandler = null;
+ removed = true;
+ }
+
+ if (!removed) {
+ // ISE for now. Could swallow this silently / log this if the ISE
+ // becomes a problem
+ throw new IllegalStateException(
+ sm.getString("wsSession.removeHandlerFailed", listener));
+ }
+ }
+
+
+ @Override
+ public String getProtocolVersion() {
+ checkState();
+ return Constants.WS_VERSION_HEADER_VALUE;
+ }
+
+
+ @Override
+ public String getNegotiatedSubprotocol() {
+ checkState();
+ return subProtocol;
+ }
+
+
+ @Override
+ public List<Extension> getNegotiatedExtensions() {
+ checkState();
+ return negotiatedExtensions;
+ }
+
+
+ @Override
+ public boolean isSecure() {
+ checkState();
+ return secure;
+ }
+
+
+ @Override
+ public boolean isOpen() {
+ return state == State.OPEN;
+ }
+
+
+ @Override
+ public long getMaxIdleTimeout() {
+ checkState();
+ return maxIdleTimeout;
+ }
+
+
+ @Override
+ public void setMaxIdleTimeout(long timeout) {
+ checkState();
+ this.maxIdleTimeout = timeout;
+ }
+
+
+ @Override
+ public void setMaxBinaryMessageBufferSize(int max) {
+ checkState();
+ this.maxBinaryMessageBufferSize = max;
+ }
+
+
+ @Override
+ public int getMaxBinaryMessageBufferSize() {
+ checkState();
+ return maxBinaryMessageBufferSize;
+ }
+
+
+ @Override
+ public void setMaxTextMessageBufferSize(int max) {
+ checkState();
+ this.maxTextMessageBufferSize = max;
+ }
+
+
+ @Override
+ public int getMaxTextMessageBufferSize() {
+ checkState();
+ return maxTextMessageBufferSize;
+ }
+
+
+ @Override
+ public Set<Session> getOpenSessions() {
+ checkState();
+ return webSocketContainer.getOpenSessions(localEndpoint);
+ }
+
+
+ @Override
+ public RemoteEndpoint.Async getAsyncRemote() {
+ checkState();
+ return remoteEndpointAsync;
+ }
+
+
+ @Override
+ public RemoteEndpoint.Basic getBasicRemote() {
+ checkState();
+ return remoteEndpointBasic;
+ }
+
+
+ @Override
+ public void close() throws IOException {
+ close(new CloseReason(CloseCodes.NORMAL_CLOSURE, ""));
+ }
+
+
+ @Override
+ public void close(CloseReason closeReason) throws IOException {
+ doClose(closeReason, closeReason);
+ }
+
+
+ /**
+ * WebSocket 1.0. Section 2.1.5.
+ * Need internal close method as spec requires that the local endpoint
+ * receives a 1006 on timeout.
+ *
+ * @param closeReasonMessage The close reason to pass to the remote endpoint
+ * @param closeReasonLocal The close reason to pass to the local endpoint
+ */
+ public void doClose(CloseReason closeReasonMessage, CloseReason closeReasonLocal) {
+ // Double-checked locking. OK because state is volatile
+ if (state != State.OPEN) {
+ return;
+ }
+
+ synchronized (stateLock) {
+ if (state != State.OPEN) {
+ return;
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("wsSession.doClose", id));
+ }
+ try {
+ wsRemoteEndpoint.setBatchingAllowed(false);
+ } catch (IOException e) {
+ log.warn(sm.getString("wsSession.flushFailOnClose"), e);
+ fireEndpointOnError(e);
+ }
+
+ state = State.OUTPUT_CLOSED;
+
+ sendCloseMessage(closeReasonMessage);
+ fireEndpointOnClose(closeReasonLocal);
+ }
+
+ IOException ioe = new IOException(sm.getString("wsSession.messageFailed"));
+ SendResult sr = new SendResult(ioe);
+ for (FutureToSendHandler f2sh : futures.keySet()) {
+ f2sh.onResult(sr);
+ }
+ }
+
+
+ /**
+ * Called when a close message is received. Should only ever happen once.
+ * Also called after a protocol error when the ProtocolHandler needs to
+ * force the closing of the connection.
+ *
+ * @param closeReason The reason contained within the received close
+ * message.
+ */
+ public void onClose(CloseReason closeReason) {
+
+ synchronized (stateLock) {
+ if (state != State.CLOSED) {
+ try {
+ wsRemoteEndpoint.setBatchingAllowed(false);
+ } catch (IOException e) {
+ log.warn(sm.getString("wsSession.flushFailOnClose"), e);
+ fireEndpointOnError(e);
+ }
+ if (state == State.OPEN) {
+ state = State.OUTPUT_CLOSED;
+ sendCloseMessage(closeReason);
+ fireEndpointOnClose(closeReason);
+ }
+ state = State.CLOSED;
+
+ // Close the socket
+ wsRemoteEndpoint.close();
+ }
+ }
+ }
+
+
+ public void onClose() {
+
+ synchronized (stateLock) {
+ if (state != State.CLOSED) {
+ try {
+ wsRemoteEndpoint.setBatchingAllowed(false);
+ } catch (IOException e) {
+ log.warn(sm.getString("wsSession.flushFailOnClose"), e);
+ fireEndpointOnError(e);
+ }
+ if (state == State.OPEN) {
+ state = State.OUTPUT_CLOSED;
+ fireEndpointOnClose(new CloseReason(
+ CloseReason.CloseCodes.NORMAL_CLOSURE, ""));
+ }
+ state = State.CLOSED;
+
+ // Close the socket
+ wsRemoteEndpoint.close();
+ }
+ }
+ }
+
+
+ private void fireEndpointOnClose(CloseReason closeReason) {
+
+ // Fire the onClose event
+ Throwable throwable = null;
+ InstanceManager instanceManager = webSocketContainer.getInstanceManager();
+ if (instanceManager == null) {
+ instanceManager = InstanceManagerBindings.get(applicationClassLoader);
+ }
+ Thread t = Thread.currentThread();
+ ClassLoader cl = t.getContextClassLoader();
+ t.setContextClassLoader(applicationClassLoader);
+ try {
+ localEndpoint.onClose(this, closeReason);
+ } catch (Throwable t1) {
+ ExceptionUtils.handleThrowable(t1);
+ throwable = t1;
+ } finally {
+ if (instanceManager != null) {
+ try {
+ instanceManager.destroyInstance(localEndpoint);
+ } catch (Throwable t2) {
+ ExceptionUtils.handleThrowable(t2);
+ if (throwable == null) {
+ throwable = t2;
+ }
+ }
+ }
+ t.setContextClassLoader(cl);
+ }
+
+ if (throwable != null) {
+ fireEndpointOnError(throwable);
+ }
+ }
+
+
+ private void fireEndpointOnError(Throwable throwable) {
+
+ // Fire the onError event
+ Thread t = Thread.currentThread();
+ ClassLoader cl = t.getContextClassLoader();
+ t.setContextClassLoader(applicationClassLoader);
+ try {
+ localEndpoint.onError(this, throwable);
+ } finally {
+ t.setContextClassLoader(cl);
+ }
+ }
+
+
+ private void sendCloseMessage(CloseReason closeReason) {
+ // 125 is maximum size for the payload of a control message
+ ByteBuffer msg = ByteBuffer.allocate(125);
+ CloseCode closeCode = closeReason.getCloseCode();
+ // CLOSED_ABNORMALLY should not be put on the wire
+ if (closeCode == CloseCodes.CLOSED_ABNORMALLY) {
+ // PROTOCOL_ERROR is probably better than GOING_AWAY here
+ msg.putShort((short) CloseCodes.PROTOCOL_ERROR.getCode());
+ } else {
+ msg.putShort((short) closeCode.getCode());
+ }
+
+ String reason = closeReason.getReasonPhrase();
+ if (reason != null && reason.length() > 0) {
+ appendCloseReasonWithTruncation(msg, reason);
+ }
+ msg.flip();
+ try {
+ wsRemoteEndpoint.sendMessageBlock(Constants.OPCODE_CLOSE, msg, true);
+ } catch (IOException | WritePendingException e) {
+ // Failed to send close message. Close the socket and let the caller
+ // deal with the Exception
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("wsSession.sendCloseFail", id), e);
+ }
+ wsRemoteEndpoint.close();
+ // Failure to send a close message is not unexpected in the case of
+ // an abnormal closure (usually triggered by a failure to read/write
+ // from/to the client. In this case do not trigger the endpoint's
+ // error handling
+ if (closeCode != CloseCodes.CLOSED_ABNORMALLY) {
+ localEndpoint.onError(this, e);
+ }
+ } finally {
+ webSocketContainer.unregisterSession(localEndpoint, this);
+ }
+ }
+
+
+ /**
+ * Use protected so unit tests can access this method directly.
+ * @param msg The message
+ * @param reason The reason
+ */
+ protected static void appendCloseReasonWithTruncation(ByteBuffer msg, String reason) {
+ // Once the close code has been added there are a maximum of 123 bytes
+ // left for the reason phrase. If it is truncated then care needs to be
+ // taken to ensure the bytes are not truncated in the middle of a
+ // multi-byte UTF-8 character.
+ byte[] reasonBytes = reason.getBytes(StandardCharsets.UTF_8);
+
+ if (reasonBytes.length <= 123) {
+ // No need to truncate
+ msg.put(reasonBytes);
+ } else {
+ // Need to truncate
+ int remaining = 123 - ELLIPSIS_BYTES_LEN;
+ int pos = 0;
+ byte[] bytesNext = reason.substring(pos, pos + 1).getBytes(StandardCharsets.UTF_8);
+ while (remaining >= bytesNext.length) {
+ msg.put(bytesNext);
+ remaining -= bytesNext.length;
+ pos++;
+ bytesNext = reason.substring(pos, pos + 1).getBytes(StandardCharsets.UTF_8);
+ }
+ msg.put(ELLIPSIS_BYTES);
+ }
+ }
+
+
+ /**
+ * Make the session aware of a {@link FutureToSendHandler} that will need to
+ * be forcibly closed if the session closes before the
+ * {@link FutureToSendHandler} completes.
+ * @param f2sh The handler
+ */
+ protected void registerFuture(FutureToSendHandler f2sh) {
+ // Ideally, this code should sync on stateLock so that the correct
+ // action is taken based on the current state of the connection.
+ // However, a sync on stateLock can't be used here as it will create the
+ // possibility of a dead-lock. See BZ 61183.
+ // Therefore, a slightly less efficient approach is used.
+
+ // Always register the future.
+ futures.put(f2sh, f2sh);
+
+ if (state == State.OPEN) {
+ // The session is open. The future has been registered with the open
+ // session. Normal processing continues.
+ return;
+ }
+
+ // The session is closed. The future may or may not have been registered
+ // in time for it to be processed during session closure.
+
+ if (f2sh.isDone()) {
+ // The future has completed. It is not known if the future was
+ // completed normally by the I/O layer or in error by doClose(). It
+ // doesn't matter which. There is nothing more to do here.
+ return;
+ }
+
+ // The session is closed. The Future had not completed when last checked.
+ // There is a small timing window that means the Future may have been
+ // completed since the last check. There is also the possibility that
+ // the Future was not registered in time to be cleaned up during session
+ // close.
+ // Attempt to complete the Future with an error result as this ensures
+ // that the Future completes and any client code waiting on it does not
+ // hang. It is slightly inefficient since the Future may have been
+ // completed in another thread or another thread may be about to
+ // complete the Future but knowing if this is the case requires the sync
+ // on stateLock (see above).
+ // Note: If multiple attempts are made to complete the Future, the
+ // second and subsequent attempts are ignored.
+
+ IOException ioe = new IOException(sm.getString("wsSession.messageFailed"));
+ SendResult sr = new SendResult(ioe);
+ f2sh.onResult(sr);
+ }
+
+
+ /**
+ * Remove a {@link FutureToSendHandler} from the set of tracked instances.
+ * @param f2sh The handler
+ */
+ protected void unregisterFuture(FutureToSendHandler f2sh) {
+ futures.remove(f2sh);
+ }
+
+
+ @Override
+ public URI getRequestURI() {
+ checkState();
+ return requestUri;
+ }
+
+
+ @Override
+ public Map<String, List<String>> getRequestParameterMap() {
+ checkState();
+ return requestParameterMap;
+ }
+
+
+ @Override
+ public String getQueryString() {
+ checkState();
+ return queryString;
+ }
+
+
+ @Override
+ public Principal getUserPrincipal() {
+ checkState();
+ return userPrincipal;
+ }
+
+
+ @Override
+ public Map<String, String> getPathParameters() {
+ checkState();
+ return pathParameters;
+ }
+
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+
+ @Override
+ public Map<String, Object> getUserProperties() {
+ checkState();
+ return userProperties;
+ }
+
+
+ public Endpoint getLocal() {
+ return localEndpoint;
+ }
+
+
+ public String getHttpSessionId() {
+ return httpSessionId;
+ }
+
+ private ByteBuffer rawFragments;
+
+ public void processFrame(ByteBuffer buf, byte opCode, boolean last)
+ throws IOException
+ {
+ if (state == State.CLOSED) {
+ return;
+ }
+
+ if (opCode == Constants.OPCODE_CONTINUATION) {
+ opCode = startOpCode;
+
+ if (rawFragments != null && rawFragments.position() > 0) {
+ rawFragments.put(buf);
+ rawFragments.flip();
+ buf = rawFragments;
+ }
+ } else {
+ if (!last && (opCode == Constants.OPCODE_BINARY ||
+ opCode == Constants.OPCODE_TEXT)) {
+ startOpCode = opCode;
+
+ if (rawFragments != null) {
+ rawFragments.clear();
+ }
+ }
+ }
+
+ if (last) {
+ startOpCode = Constants.OPCODE_CONTINUATION;
+ }
+
+ if (opCode == Constants.OPCODE_PONG) {
+ if (pongMessageHandler != null) {
+ final ByteBuffer b = buf;
+
+ PongMessage pongMessage = new PongMessage() {
+ @Override
+ public ByteBuffer getApplicationData() {
+ return b;
+ }
+ };
+
+ pongMessageHandler.onMessage(pongMessage);
+ }
+ }
+
+ if (opCode == Constants.OPCODE_CLOSE) {
+ CloseReason closeReason;
+
+ if (buf.remaining() >= 2) {
+ short closeCode = buf.order(ByteOrder.BIG_ENDIAN).getShort();
+
+ closeReason = new CloseReason(
+ CloseReason.CloseCodes.getCloseCode(closeCode),
+ buf.asCharBuffer().toString());
+ } else {
+ closeReason = new CloseReason(
+ CloseReason.CloseCodes.NORMAL_CLOSURE, "");
+ }
+
+ onClose(closeReason);
+ }
+
+ if (opCode == Constants.OPCODE_BINARY) {
+ onMessage(buf, last);
+ }
+
+ if (opCode == Constants.OPCODE_TEXT) {
+ if (messageBufferText.position() == 0 && maxTextMessageBufferSize != messageBufferText.capacity()) {
+ messageBufferText = CharBuffer.allocate(maxTextMessageBufferSize);
+ }
+
+ CoderResult cr = utf8DecoderMessage.decode(buf, messageBufferText, last);
+ if (cr.isError()) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.NOT_CONSISTENT,
+ sm.getString("wsFrame.invalidUtf8")));
+ } else if (cr.isOverflow()) {
+ // Ran out of space in text buffer - flush it
+ if (hasTextPartial()) {
+ do {
+ onMessage(messageBufferText, false);
+
+ cr = utf8DecoderMessage.decode(buf, messageBufferText, last);
+ } while (cr.isOverflow());
+ } else {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.TOO_BIG,
+ sm.getString("wsFrame.textMessageTooBig")));
+ }
+ } else if (cr.isUnderflow() && !last) {
+ updateRawFragments(buf, last);
+
+ if (hasTextPartial()) {
+ onMessage(messageBufferText, false);
+ }
+
+ return;
+ }
+
+ if (last) {
+ utf8DecoderMessage.reset();
+ }
+
+ updateRawFragments(buf, last);
+
+ onMessage(messageBufferText, last);
+ }
+ }
+
+
+ private boolean hasTextPartial() {
+ return textMessageHandler instanceof MessageHandler.Partial<?>;
+ }
+
+
+ private void onMessage(CharBuffer buf, boolean last) throws IOException {
+ buf.flip();
+ try {
+ onMessage(buf.toString(), last);
+ } catch (Throwable t) {
+ handleThrowableOnSend(t);
+ } finally {
+ buf.clear();
+ }
+ }
+
+
+ private void updateRawFragments(ByteBuffer buf, boolean last) {
+ if (!last && buf.remaining() > 0) {
+ if (buf == rawFragments) {
+ buf.compact();
+ } else {
+ if (rawFragments == null || (rawFragments.position() == 0 && maxTextMessageBufferSize != rawFragments.capacity())) {
+ rawFragments = ByteBuffer.allocateDirect(maxTextMessageBufferSize);
+ }
+ rawFragments.put(buf);
+ }
+ } else {
+ if (rawFragments != null) {
+ rawFragments.clear();
+ }
+ }
+ }
+
+
+ @SuppressWarnings("unchecked")
+ public void onMessage(String text, boolean last) {
+ if (hasTextPartial()) {
+ ((MessageHandler.Partial<String>) textMessageHandler).onMessage(text, last);
+ } else {
+ // Caller ensures last == true if this branch is used
+ ((MessageHandler.Whole<String>) textMessageHandler).onMessage(text);
+ }
+ }
+
+
+ @SuppressWarnings("unchecked")
+ public void onMessage(ByteBuffer buf, boolean last)
+ throws IOException
+ {
+ if (binaryMessageHandler instanceof MessageHandler.Partial<?>) {
+ ((MessageHandler.Partial<ByteBuffer>) binaryMessageHandler).onMessage(buf, last);
+ } else {
+ if (last && (binaryBuffer == null || binaryBuffer.position() == 0)) {
+ ((MessageHandler.Whole<ByteBuffer>) binaryMessageHandler).onMessage(buf);
+ return;
+ }
+
+ if (binaryBuffer == null ||
+ (binaryBuffer.position() == 0 && binaryBuffer.capacity() != maxBinaryMessageBufferSize))
+ {
+ binaryBuffer = ByteBuffer.allocateDirect(maxBinaryMessageBufferSize);
+ }
+
+ if (binaryBuffer.remaining() < buf.remaining()) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.TOO_BIG,
+ sm.getString("wsFrame.textMessageTooBig")));
+ }
+
+ binaryBuffer.put(buf);
+
+ if (last) {
+ binaryBuffer.flip();
+ try {
+ ((MessageHandler.Whole<ByteBuffer>) binaryMessageHandler).onMessage(binaryBuffer);
+ } finally {
+ binaryBuffer.clear();
+ }
+ }
+ }
+ }
+
+
+ private void handleThrowableOnSend(Throwable t) throws WsIOException {
+ ExceptionUtils.handleThrowable(t);
+ getLocal().onError(this, t);
+ CloseReason cr = new CloseReason(CloseCodes.CLOSED_ABNORMALLY,
+ sm.getString("wsFrame.ioeTriggeredClose"));
+ throw new WsIOException(cr);
+ }
+
+
+ protected MessageHandler getTextMessageHandler() {
+ return textMessageHandler;
+ }
+
+
+ protected MessageHandler getBinaryMessageHandler() {
+ return binaryMessageHandler;
+ }
+
+
+ protected MessageHandler.Whole<PongMessage> getPongMessageHandler() {
+ return pongMessageHandler;
+ }
+
+
+ protected void updateLastActive() {
+ lastActive = System.currentTimeMillis();
+ }
+
+
+ protected void checkExpiration() {
+ long timeout = maxIdleTimeout;
+ if (timeout < 1) {
+ return;
+ }
+
+ if (System.currentTimeMillis() - lastActive > timeout) {
+ String msg = sm.getString("wsSession.timeout", getId());
+ if (log.isDebugEnabled()) {
+ log.debug(msg);
+ }
+ doClose(new CloseReason(CloseCodes.GOING_AWAY, msg),
+ new CloseReason(CloseCodes.CLOSED_ABNORMALLY, msg));
+ }
+ }
+
+
+ private void checkState() {
+ if (state == State.CLOSED) {
+ /*
+ * As per RFC 6455, a WebSocket connection is considered to be
+ * closed once a peer has sent and received a WebSocket close frame.
+ */
+ throw new IllegalStateException(sm.getString("wsSession.closed", id));
+ }
+ }
+
+ private enum State {
+ OPEN,
+ OUTPUT_CLOSED,
+ CLOSED
+ }
+}
diff --git a/src/java/nginx/unit/websocket/WsWebSocketContainer.java b/src/java/nginx/unit/websocket/WsWebSocketContainer.java
new file mode 100644
index 00000000..282665ef
--- /dev/null
+++ b/src/java/nginx/unit/websocket/WsWebSocketContainer.java
@@ -0,0 +1,1123 @@
+/*
+ * 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 nginx.unit.websocket;
+
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousChannelGroup;
+import java.nio.channels.AsynchronousSocketChannel;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.TrustManagerFactory;
+import javax.websocket.ClientEndpoint;
+import javax.websocket.ClientEndpointConfig;
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCodes;
+import javax.websocket.DeploymentException;
+import javax.websocket.Endpoint;
+import javax.websocket.Extension;
+import javax.websocket.HandshakeResponse;
+import javax.websocket.Session;
+import javax.websocket.WebSocketContainer;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.InstanceManager;
+import org.apache.tomcat.util.buf.StringUtils;
+import org.apache.tomcat.util.codec.binary.Base64;
+import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap;
+import org.apache.tomcat.util.res.StringManager;
+import nginx.unit.websocket.pojo.PojoEndpointClient;
+
+public class WsWebSocketContainer implements WebSocketContainer, BackgroundProcess {
+
+ private static final StringManager sm = StringManager.getManager(WsWebSocketContainer.class);
+ private static final Random RANDOM = new Random();
+ private static final byte[] CRLF = new byte[] { 13, 10 };
+
+ private static final byte[] GET_BYTES = "GET ".getBytes(StandardCharsets.ISO_8859_1);
+ private static final byte[] ROOT_URI_BYTES = "/".getBytes(StandardCharsets.ISO_8859_1);
+ private static final byte[] HTTP_VERSION_BYTES =
+ " HTTP/1.1\r\n".getBytes(StandardCharsets.ISO_8859_1);
+
+ private volatile AsynchronousChannelGroup asynchronousChannelGroup = null;
+ private final Object asynchronousChannelGroupLock = new Object();
+
+ private final Log log = LogFactory.getLog(WsWebSocketContainer.class); // must not be static
+ private final Map<Endpoint, Set<WsSession>> endpointSessionMap =
+ new HashMap<>();
+ private final Map<WsSession,WsSession> sessions = new ConcurrentHashMap<>();
+ private final Object endPointSessionMapLock = new Object();
+
+ private long defaultAsyncTimeout = -1;
+ private int maxBinaryMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;
+ private int maxTextMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;
+ private volatile long defaultMaxSessionIdleTimeout = 0;
+ private int backgroundProcessCount = 0;
+ private int processPeriod = Constants.DEFAULT_PROCESS_PERIOD;
+
+ private InstanceManager instanceManager;
+
+ InstanceManager getInstanceManager() {
+ return instanceManager;
+ }
+
+ protected void setInstanceManager(InstanceManager instanceManager) {
+ this.instanceManager = instanceManager;
+ }
+
+ @Override
+ public Session connectToServer(Object pojo, URI path)
+ throws DeploymentException {
+
+ ClientEndpoint annotation =
+ pojo.getClass().getAnnotation(ClientEndpoint.class);
+ if (annotation == null) {
+ throw new DeploymentException(
+ sm.getString("wsWebSocketContainer.missingAnnotation",
+ pojo.getClass().getName()));
+ }
+
+ Endpoint ep = new PojoEndpointClient(pojo, Arrays.asList(annotation.decoders()));
+
+ Class<? extends ClientEndpointConfig.Configurator> configuratorClazz =
+ annotation.configurator();
+
+ ClientEndpointConfig.Configurator configurator = null;
+ if (!ClientEndpointConfig.Configurator.class.equals(
+ configuratorClazz)) {
+ try {
+ configurator = configuratorClazz.getConstructor().newInstance();
+ } catch (ReflectiveOperationException e) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.defaultConfiguratorFail"), e);
+ }
+ }
+
+ ClientEndpointConfig.Builder builder = ClientEndpointConfig.Builder.create();
+ // Avoid NPE when using RI API JAR - see BZ 56343
+ if (configurator != null) {
+ builder.configurator(configurator);
+ }
+ ClientEndpointConfig config = builder.
+ decoders(Arrays.asList(annotation.decoders())).
+ encoders(Arrays.asList(annotation.encoders())).
+ preferredSubprotocols(Arrays.asList(annotation.subprotocols())).
+ build();
+ return connectToServer(ep, config, path);
+ }
+
+
+ @Override
+ public Session connectToServer(Class<?> annotatedEndpointClass, URI path)
+ throws DeploymentException {
+
+ Object pojo;
+ try {
+ pojo = annotatedEndpointClass.getConstructor().newInstance();
+ } catch (ReflectiveOperationException e) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.endpointCreateFail",
+ annotatedEndpointClass.getName()), e);
+ }
+
+ return connectToServer(pojo, path);
+ }
+
+
+ @Override
+ public Session connectToServer(Class<? extends Endpoint> clazz,
+ ClientEndpointConfig clientEndpointConfiguration, URI path)
+ throws DeploymentException {
+
+ Endpoint endpoint;
+ try {
+ endpoint = clazz.getConstructor().newInstance();
+ } catch (ReflectiveOperationException e) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.endpointCreateFail", clazz.getName()),
+ e);
+ }
+
+ return connectToServer(endpoint, clientEndpointConfiguration, path);
+ }
+
+
+ @Override
+ public Session connectToServer(Endpoint endpoint,
+ ClientEndpointConfig clientEndpointConfiguration, URI path)
+ throws DeploymentException {
+ return connectToServerRecursive(endpoint, clientEndpointConfiguration, path, new HashSet<>());
+ }
+
+ private Session connectToServerRecursive(Endpoint endpoint,
+ ClientEndpointConfig clientEndpointConfiguration, URI path,
+ Set<URI> redirectSet)
+ throws DeploymentException {
+
+ boolean secure = false;
+ ByteBuffer proxyConnect = null;
+ URI proxyPath;
+
+ // Validate scheme (and build proxyPath)
+ String scheme = path.getScheme();
+ if ("ws".equalsIgnoreCase(scheme)) {
+ proxyPath = URI.create("http" + path.toString().substring(2));
+ } else if ("wss".equalsIgnoreCase(scheme)) {
+ proxyPath = URI.create("https" + path.toString().substring(3));
+ secure = true;
+ } else {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.pathWrongScheme", scheme));
+ }
+
+ // Validate host
+ String host = path.getHost();
+ if (host == null) {
+ throw new DeploymentException(
+ sm.getString("wsWebSocketContainer.pathNoHost"));
+ }
+ int port = path.getPort();
+
+ SocketAddress sa = null;
+
+ // Check to see if a proxy is configured. Javadoc indicates return value
+ // will never be null
+ List<Proxy> proxies = ProxySelector.getDefault().select(proxyPath);
+ Proxy selectedProxy = null;
+ for (Proxy proxy : proxies) {
+ if (proxy.type().equals(Proxy.Type.HTTP)) {
+ sa = proxy.address();
+ if (sa instanceof InetSocketAddress) {
+ InetSocketAddress inet = (InetSocketAddress) sa;
+ if (inet.isUnresolved()) {
+ sa = new InetSocketAddress(inet.getHostName(), inet.getPort());
+ }
+ }
+ selectedProxy = proxy;
+ break;
+ }
+ }
+
+ // If the port is not explicitly specified, compute it based on the
+ // scheme
+ if (port == -1) {
+ if ("ws".equalsIgnoreCase(scheme)) {
+ port = 80;
+ } else {
+ // Must be wss due to scheme validation above
+ port = 443;
+ }
+ }
+
+ // If sa is null, no proxy is configured so need to create sa
+ if (sa == null) {
+ sa = new InetSocketAddress(host, port);
+ } else {
+ proxyConnect = createProxyRequest(host, port);
+ }
+
+ // Create the initial HTTP request to open the WebSocket connection
+ Map<String, List<String>> reqHeaders = createRequestHeaders(host, port,
+ clientEndpointConfiguration);
+ clientEndpointConfiguration.getConfigurator().beforeRequest(reqHeaders);
+ if (Constants.DEFAULT_ORIGIN_HEADER_VALUE != null
+ && !reqHeaders.containsKey(Constants.ORIGIN_HEADER_NAME)) {
+ List<String> originValues = new ArrayList<>(1);
+ originValues.add(Constants.DEFAULT_ORIGIN_HEADER_VALUE);
+ reqHeaders.put(Constants.ORIGIN_HEADER_NAME, originValues);
+ }
+ ByteBuffer request = createRequest(path, reqHeaders);
+
+ AsynchronousSocketChannel socketChannel;
+ try {
+ socketChannel = AsynchronousSocketChannel.open(getAsynchronousChannelGroup());
+ } catch (IOException ioe) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.asynchronousSocketChannelFail"), ioe);
+ }
+
+ Map<String,Object> userProperties = clientEndpointConfiguration.getUserProperties();
+
+ // Get the connection timeout
+ long timeout = Constants.IO_TIMEOUT_MS_DEFAULT;
+ String timeoutValue = (String) userProperties.get(Constants.IO_TIMEOUT_MS_PROPERTY);
+ if (timeoutValue != null) {
+ timeout = Long.valueOf(timeoutValue).intValue();
+ }
+
+ // Set-up
+ // Same size as the WsFrame input buffer
+ ByteBuffer response = ByteBuffer.allocate(getDefaultMaxBinaryMessageBufferSize());
+ String subProtocol;
+ boolean success = false;
+ List<Extension> extensionsAgreed = new ArrayList<>();
+ Transformation transformation = null;
+
+ // Open the connection
+ Future<Void> fConnect = socketChannel.connect(sa);
+ AsyncChannelWrapper channel = null;
+
+ if (proxyConnect != null) {
+ try {
+ fConnect.get(timeout, TimeUnit.MILLISECONDS);
+ // Proxy CONNECT is clear text
+ channel = new AsyncChannelWrapperNonSecure(socketChannel);
+ writeRequest(channel, proxyConnect, timeout);
+ HttpResponse httpResponse = processResponse(response, channel, timeout);
+ if (httpResponse.getStatus() != 200) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.proxyConnectFail", selectedProxy,
+ Integer.toString(httpResponse.getStatus())));
+ }
+ } catch (TimeoutException | InterruptedException | ExecutionException |
+ EOFException e) {
+ if (channel != null) {
+ channel.close();
+ }
+ throw new DeploymentException(
+ sm.getString("wsWebSocketContainer.httpRequestFailed"), e);
+ }
+ }
+
+ if (secure) {
+ // Regardless of whether a non-secure wrapper was created for a
+ // proxy CONNECT, need to use TLS from this point on so wrap the
+ // original AsynchronousSocketChannel
+ SSLEngine sslEngine = createSSLEngine(userProperties, host, port);
+ channel = new AsyncChannelWrapperSecure(socketChannel, sslEngine);
+ } else if (channel == null) {
+ // Only need to wrap as this point if it wasn't wrapped to process a
+ // proxy CONNECT
+ channel = new AsyncChannelWrapperNonSecure(socketChannel);
+ }
+
+ try {
+ fConnect.get(timeout, TimeUnit.MILLISECONDS);
+
+ Future<Void> fHandshake = channel.handshake();
+ fHandshake.get(timeout, TimeUnit.MILLISECONDS);
+
+ writeRequest(channel, request, timeout);
+
+ HttpResponse httpResponse = processResponse(response, channel, timeout);
+
+ // Check maximum permitted redirects
+ int maxRedirects = Constants.MAX_REDIRECTIONS_DEFAULT;
+ String maxRedirectsValue =
+ (String) userProperties.get(Constants.MAX_REDIRECTIONS_PROPERTY);
+ if (maxRedirectsValue != null) {
+ maxRedirects = Integer.parseInt(maxRedirectsValue);
+ }
+
+ if (httpResponse.status != 101) {
+ if(isRedirectStatus(httpResponse.status)){
+ List<String> locationHeader =
+ httpResponse.getHandshakeResponse().getHeaders().get(
+ Constants.LOCATION_HEADER_NAME);
+
+ if (locationHeader == null || locationHeader.isEmpty() ||
+ locationHeader.get(0) == null || locationHeader.get(0).isEmpty()) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.missingLocationHeader",
+ Integer.toString(httpResponse.status)));
+ }
+
+ URI redirectLocation = URI.create(locationHeader.get(0)).normalize();
+
+ if (!redirectLocation.isAbsolute()) {
+ redirectLocation = path.resolve(redirectLocation);
+ }
+
+ String redirectScheme = redirectLocation.getScheme().toLowerCase();
+
+ if (redirectScheme.startsWith("http")) {
+ redirectLocation = new URI(redirectScheme.replace("http", "ws"),
+ redirectLocation.getUserInfo(), redirectLocation.getHost(),
+ redirectLocation.getPort(), redirectLocation.getPath(),
+ redirectLocation.getQuery(), redirectLocation.getFragment());
+ }
+
+ if (!redirectSet.add(redirectLocation) || redirectSet.size() > maxRedirects) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.redirectThreshold", redirectLocation,
+ Integer.toString(redirectSet.size()),
+ Integer.toString(maxRedirects)));
+ }
+
+ return connectToServerRecursive(endpoint, clientEndpointConfiguration, redirectLocation, redirectSet);
+
+ }
+
+ else if (httpResponse.status == 401) {
+
+ if (userProperties.get(Constants.AUTHORIZATION_HEADER_NAME) != null) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.failedAuthentication",
+ Integer.valueOf(httpResponse.status)));
+ }
+
+ List<String> wwwAuthenticateHeaders = httpResponse.getHandshakeResponse()
+ .getHeaders().get(Constants.WWW_AUTHENTICATE_HEADER_NAME);
+
+ if (wwwAuthenticateHeaders == null || wwwAuthenticateHeaders.isEmpty() ||
+ wwwAuthenticateHeaders.get(0) == null || wwwAuthenticateHeaders.get(0).isEmpty()) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.missingWWWAuthenticateHeader",
+ Integer.toString(httpResponse.status)));
+ }
+
+ String authScheme = wwwAuthenticateHeaders.get(0).split("\\s+", 2)[0];
+ String requestUri = new String(request.array(), StandardCharsets.ISO_8859_1)
+ .split("\\s", 3)[1];
+
+ Authenticator auth = AuthenticatorFactory.getAuthenticator(authScheme);
+
+ if (auth == null) {
+ throw new DeploymentException(
+ sm.getString("wsWebSocketContainer.unsupportedAuthScheme",
+ Integer.valueOf(httpResponse.status), authScheme));
+ }
+
+ userProperties.put(Constants.AUTHORIZATION_HEADER_NAME, auth.getAuthorization(
+ requestUri, wwwAuthenticateHeaders.get(0), userProperties));
+
+ return connectToServerRecursive(endpoint, clientEndpointConfiguration, path, redirectSet);
+
+ }
+
+ else {
+ throw new DeploymentException(sm.getString("wsWebSocketContainer.invalidStatus",
+ Integer.toString(httpResponse.status)));
+ }
+ }
+ HandshakeResponse handshakeResponse = httpResponse.getHandshakeResponse();
+ clientEndpointConfiguration.getConfigurator().afterResponse(handshakeResponse);
+
+ // Sub-protocol
+ List<String> protocolHeaders = handshakeResponse.getHeaders().get(
+ Constants.WS_PROTOCOL_HEADER_NAME);
+ if (protocolHeaders == null || protocolHeaders.size() == 0) {
+ subProtocol = null;
+ } else if (protocolHeaders.size() == 1) {
+ subProtocol = protocolHeaders.get(0);
+ } else {
+ throw new DeploymentException(
+ sm.getString("wsWebSocketContainer.invalidSubProtocol"));
+ }
+
+ // Extensions
+ // Should normally only be one header but handle the case of
+ // multiple headers
+ List<String> extHeaders = handshakeResponse.getHeaders().get(
+ Constants.WS_EXTENSIONS_HEADER_NAME);
+ if (extHeaders != null) {
+ for (String extHeader : extHeaders) {
+ Util.parseExtensionHeader(extensionsAgreed, extHeader);
+ }
+ }
+
+ // Build the transformations
+ TransformationFactory factory = TransformationFactory.getInstance();
+ for (Extension extension : extensionsAgreed) {
+ List<List<Extension.Parameter>> wrapper = new ArrayList<>(1);
+ wrapper.add(extension.getParameters());
+ Transformation t = factory.create(extension.getName(), wrapper, false);
+ if (t == null) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.invalidExtensionParameters"));
+ }
+ if (transformation == null) {
+ transformation = t;
+ } else {
+ transformation.setNext(t);
+ }
+ }
+
+ success = true;
+ } catch (ExecutionException | InterruptedException | SSLException |
+ EOFException | TimeoutException | URISyntaxException | AuthenticationException e) {
+ throw new DeploymentException(
+ sm.getString("wsWebSocketContainer.httpRequestFailed"), e);
+ } finally {
+ if (!success) {
+ channel.close();
+ }
+ }
+
+ // Switch to WebSocket
+ WsRemoteEndpointImplClient wsRemoteEndpointClient = new WsRemoteEndpointImplClient(channel);
+
+ WsSession wsSession = new WsSession(endpoint, wsRemoteEndpointClient,
+ this, null, null, null, null, null, extensionsAgreed,
+ subProtocol, Collections.<String,String>emptyMap(), secure,
+ clientEndpointConfiguration, null);
+
+ WsFrameClient wsFrameClient = new WsFrameClient(response, channel,
+ wsSession, transformation);
+ // WsFrame adds the necessary final transformations. Copy the
+ // completed transformation chain to the remote end point.
+ wsRemoteEndpointClient.setTransformation(wsFrameClient.getTransformation());
+
+ endpoint.onOpen(wsSession, clientEndpointConfiguration);
+ registerSession(endpoint, wsSession);
+
+ /* It is possible that the server sent one or more messages as soon as
+ * the WebSocket connection was established. Depending on the exact
+ * timing of when those messages were sent they could be sat in the
+ * input buffer waiting to be read and will not trigger a "data
+ * available to read" event. Therefore, it is necessary to process the
+ * input buffer here. Note that this happens on the current thread which
+ * means that this thread will be used for any onMessage notifications.
+ * This is a special case. Subsequent "data available to read" events
+ * will be handled by threads from the AsyncChannelGroup's executor.
+ */
+ wsFrameClient.startInputProcessing();
+
+ return wsSession;
+ }
+
+
+ private static void writeRequest(AsyncChannelWrapper channel, ByteBuffer request,
+ long timeout) throws TimeoutException, InterruptedException, ExecutionException {
+ int toWrite = request.limit();
+
+ Future<Integer> fWrite = channel.write(request);
+ Integer thisWrite = fWrite.get(timeout, TimeUnit.MILLISECONDS);
+ toWrite -= thisWrite.intValue();
+
+ while (toWrite > 0) {
+ fWrite = channel.write(request);
+ thisWrite = fWrite.get(timeout, TimeUnit.MILLISECONDS);
+ toWrite -= thisWrite.intValue();
+ }
+ }
+
+
+ private static boolean isRedirectStatus(int httpResponseCode) {
+
+ boolean isRedirect = false;
+
+ switch (httpResponseCode) {
+ case Constants.MULTIPLE_CHOICES:
+ case Constants.MOVED_PERMANENTLY:
+ case Constants.FOUND:
+ case Constants.SEE_OTHER:
+ case Constants.USE_PROXY:
+ case Constants.TEMPORARY_REDIRECT:
+ isRedirect = true;
+ break;
+ default:
+ break;
+ }
+
+ return isRedirect;
+ }
+
+
+ private static ByteBuffer createProxyRequest(String host, int port) {
+ StringBuilder request = new StringBuilder();
+ request.append("CONNECT ");
+ request.append(host);
+ request.append(':');
+ request.append(port);
+
+ request.append(" HTTP/1.1\r\nProxy-Connection: keep-alive\r\nConnection: keepalive\r\nHost: ");
+ request.append(host);
+ request.append(':');
+ request.append(port);
+
+ request.append("\r\n\r\n");
+
+ byte[] bytes = request.toString().getBytes(StandardCharsets.ISO_8859_1);
+ return ByteBuffer.wrap(bytes);
+ }
+
+ protected void registerSession(Endpoint endpoint, WsSession wsSession) {
+
+ if (!wsSession.isOpen()) {
+ // The session was closed during onOpen. No need to register it.
+ return;
+ }
+ synchronized (endPointSessionMapLock) {
+ if (endpointSessionMap.size() == 0) {
+ BackgroundProcessManager.getInstance().register(this);
+ }
+ Set<WsSession> wsSessions = endpointSessionMap.get(endpoint);
+ if (wsSessions == null) {
+ wsSessions = new HashSet<>();
+ endpointSessionMap.put(endpoint, wsSessions);
+ }
+ wsSessions.add(wsSession);
+ }
+ sessions.put(wsSession, wsSession);
+ }
+
+
+ protected void unregisterSession(Endpoint endpoint, WsSession wsSession) {
+
+ synchronized (endPointSessionMapLock) {
+ Set<WsSession> wsSessions = endpointSessionMap.get(endpoint);
+ if (wsSessions != null) {
+ wsSessions.remove(wsSession);
+ if (wsSessions.size() == 0) {
+ endpointSessionMap.remove(endpoint);
+ }
+ }
+ if (endpointSessionMap.size() == 0) {
+ BackgroundProcessManager.getInstance().unregister(this);
+ }
+ }
+ sessions.remove(wsSession);
+ }
+
+
+ Set<Session> getOpenSessions(Endpoint endpoint) {
+ HashSet<Session> result = new HashSet<>();
+ synchronized (endPointSessionMapLock) {
+ Set<WsSession> sessions = endpointSessionMap.get(endpoint);
+ if (sessions != null) {
+ result.addAll(sessions);
+ }
+ }
+ return result;
+ }
+
+ private static Map<String, List<String>> createRequestHeaders(String host, int port,
+ ClientEndpointConfig clientEndpointConfiguration) {
+
+ Map<String, List<String>> headers = new HashMap<>();
+ List<Extension> extensions = clientEndpointConfiguration.getExtensions();
+ List<String> subProtocols = clientEndpointConfiguration.getPreferredSubprotocols();
+ Map<String, Object> userProperties = clientEndpointConfiguration.getUserProperties();
+
+ if (userProperties.get(Constants.AUTHORIZATION_HEADER_NAME) != null) {
+ List<String> authValues = new ArrayList<>(1);
+ authValues.add((String) userProperties.get(Constants.AUTHORIZATION_HEADER_NAME));
+ headers.put(Constants.AUTHORIZATION_HEADER_NAME, authValues);
+ }
+
+ // Host header
+ List<String> hostValues = new ArrayList<>(1);
+ if (port == -1) {
+ hostValues.add(host);
+ } else {
+ hostValues.add(host + ':' + port);
+ }
+
+ headers.put(Constants.HOST_HEADER_NAME, hostValues);
+
+ // Upgrade header
+ List<String> upgradeValues = new ArrayList<>(1);
+ upgradeValues.add(Constants.UPGRADE_HEADER_VALUE);
+ headers.put(Constants.UPGRADE_HEADER_NAME, upgradeValues);
+
+ // Connection header
+ List<String> connectionValues = new ArrayList<>(1);
+ connectionValues.add(Constants.CONNECTION_HEADER_VALUE);
+ headers.put(Constants.CONNECTION_HEADER_NAME, connectionValues);
+
+ // WebSocket version header
+ List<String> wsVersionValues = new ArrayList<>(1);
+ wsVersionValues.add(Constants.WS_VERSION_HEADER_VALUE);
+ headers.put(Constants.WS_VERSION_HEADER_NAME, wsVersionValues);
+
+ // WebSocket key
+ List<String> wsKeyValues = new ArrayList<>(1);
+ wsKeyValues.add(generateWsKeyValue());
+ headers.put(Constants.WS_KEY_HEADER_NAME, wsKeyValues);
+
+ // WebSocket sub-protocols
+ if (subProtocols != null && subProtocols.size() > 0) {
+ headers.put(Constants.WS_PROTOCOL_HEADER_NAME, subProtocols);
+ }
+
+ // WebSocket extensions
+ if (extensions != null && extensions.size() > 0) {
+ headers.put(Constants.WS_EXTENSIONS_HEADER_NAME,
+ generateExtensionHeaders(extensions));
+ }
+
+ return headers;
+ }
+
+
+ private static List<String> generateExtensionHeaders(List<Extension> extensions) {
+ List<String> result = new ArrayList<>(extensions.size());
+ for (Extension extension : extensions) {
+ StringBuilder header = new StringBuilder();
+ header.append(extension.getName());
+ for (Extension.Parameter param : extension.getParameters()) {
+ header.append(';');
+ header.append(param.getName());
+ String value = param.getValue();
+ if (value != null && value.length() > 0) {
+ header.append('=');
+ header.append(value);
+ }
+ }
+ result.add(header.toString());
+ }
+ return result;
+ }
+
+
+ private static String generateWsKeyValue() {
+ byte[] keyBytes = new byte[16];
+ RANDOM.nextBytes(keyBytes);
+ return Base64.encodeBase64String(keyBytes);
+ }
+
+
+ private static ByteBuffer createRequest(URI uri, Map<String,List<String>> reqHeaders) {
+ ByteBuffer result = ByteBuffer.allocate(4 * 1024);
+
+ // Request line
+ result.put(GET_BYTES);
+ if (null == uri.getPath() || "".equals(uri.getPath())) {
+ result.put(ROOT_URI_BYTES);
+ } else {
+ result.put(uri.getRawPath().getBytes(StandardCharsets.ISO_8859_1));
+ }
+ String query = uri.getRawQuery();
+ if (query != null) {
+ result.put((byte) '?');
+ result.put(query.getBytes(StandardCharsets.ISO_8859_1));
+ }
+ result.put(HTTP_VERSION_BYTES);
+
+ // Headers
+ for (Entry<String, List<String>> entry : reqHeaders.entrySet()) {
+ result = addHeader(result, entry.getKey(), entry.getValue());
+ }
+
+ // Terminating CRLF
+ result.put(CRLF);
+
+ result.flip();
+
+ return result;
+ }
+
+
+ private static ByteBuffer addHeader(ByteBuffer result, String key, List<String> values) {
+ if (values.isEmpty()) {
+ return result;
+ }
+
+ result = putWithExpand(result, key.getBytes(StandardCharsets.ISO_8859_1));
+ result = putWithExpand(result, ": ".getBytes(StandardCharsets.ISO_8859_1));
+ result = putWithExpand(result, StringUtils.join(values).getBytes(StandardCharsets.ISO_8859_1));
+ result = putWithExpand(result, CRLF);
+
+ return result;
+ }
+
+
+ private static ByteBuffer putWithExpand(ByteBuffer input, byte[] bytes) {
+ if (bytes.length > input.remaining()) {
+ int newSize;
+ if (bytes.length > input.capacity()) {
+ newSize = 2 * bytes.length;
+ } else {
+ newSize = input.capacity() * 2;
+ }
+ ByteBuffer expanded = ByteBuffer.allocate(newSize);
+ input.flip();
+ expanded.put(input);
+ input = expanded;
+ }
+ return input.put(bytes);
+ }
+
+
+ /**
+ * Process response, blocking until HTTP response has been fully received.
+ * @throws ExecutionException
+ * @throws InterruptedException
+ * @throws DeploymentException
+ * @throws TimeoutException
+ */
+ private HttpResponse processResponse(ByteBuffer response,
+ AsyncChannelWrapper channel, long timeout) throws InterruptedException,
+ ExecutionException, DeploymentException, EOFException,
+ TimeoutException {
+
+ Map<String,List<String>> headers = new CaseInsensitiveKeyMap<>();
+
+ int status = 0;
+ boolean readStatus = false;
+ boolean readHeaders = false;
+ String line = null;
+ while (!readHeaders) {
+ // On entering loop buffer will be empty and at the start of a new
+ // loop the buffer will have been fully read.
+ response.clear();
+ // Blocking read
+ Future<Integer> read = channel.read(response);
+ Integer bytesRead = read.get(timeout, TimeUnit.MILLISECONDS);
+ if (bytesRead.intValue() == -1) {
+ throw new EOFException();
+ }
+ response.flip();
+ while (response.hasRemaining() && !readHeaders) {
+ if (line == null) {
+ line = readLine(response);
+ } else {
+ line += readLine(response);
+ }
+ if ("\r\n".equals(line)) {
+ readHeaders = true;
+ } else if (line.endsWith("\r\n")) {
+ if (readStatus) {
+ parseHeaders(line, headers);
+ } else {
+ status = parseStatus(line);
+ readStatus = true;
+ }
+ line = null;
+ }
+ }
+ }
+
+ return new HttpResponse(status, new WsHandshakeResponse(headers));
+ }
+
+
+ private int parseStatus(String line) throws DeploymentException {
+ // This client only understands HTTP 1.
+ // RFC2616 is case specific
+ String[] parts = line.trim().split(" ");
+ // CONNECT for proxy may return a 1.0 response
+ if (parts.length < 2 || !("HTTP/1.0".equals(parts[0]) || "HTTP/1.1".equals(parts[0]))) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.invalidStatus", line));
+ }
+ try {
+ return Integer.parseInt(parts[1]);
+ } catch (NumberFormatException nfe) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.invalidStatus", line));
+ }
+ }
+
+
+ private void parseHeaders(String line, Map<String,List<String>> headers) {
+ // Treat headers as single values by default.
+
+ int index = line.indexOf(':');
+ if (index == -1) {
+ log.warn(sm.getString("wsWebSocketContainer.invalidHeader", line));
+ return;
+ }
+ // Header names are case insensitive so always use lower case
+ String headerName = line.substring(0, index).trim().toLowerCase(Locale.ENGLISH);
+ // Multi-value headers are stored as a single header and the client is
+ // expected to handle splitting into individual values
+ String headerValue = line.substring(index + 1).trim();
+
+ List<String> values = headers.get(headerName);
+ if (values == null) {
+ values = new ArrayList<>(1);
+ headers.put(headerName, values);
+ }
+ values.add(headerValue);
+ }
+
+ private String readLine(ByteBuffer response) {
+ // All ISO-8859-1
+ StringBuilder sb = new StringBuilder();
+
+ char c = 0;
+ while (response.hasRemaining()) {
+ c = (char) response.get();
+ sb.append(c);
+ if (c == 10) {
+ break;
+ }
+ }
+
+ return sb.toString();
+ }
+
+
+ private SSLEngine createSSLEngine(Map<String,Object> userProperties, String host, int port)
+ throws DeploymentException {
+
+ try {
+ // See if a custom SSLContext has been provided
+ SSLContext sslContext =
+ (SSLContext) userProperties.get(Constants.SSL_CONTEXT_PROPERTY);
+
+ if (sslContext == null) {
+ // Create the SSL Context
+ sslContext = SSLContext.getInstance("TLS");
+
+ // Trust store
+ String sslTrustStoreValue =
+ (String) userProperties.get(Constants.SSL_TRUSTSTORE_PROPERTY);
+ if (sslTrustStoreValue != null) {
+ String sslTrustStorePwdValue = (String) userProperties.get(
+ Constants.SSL_TRUSTSTORE_PWD_PROPERTY);
+ if (sslTrustStorePwdValue == null) {
+ sslTrustStorePwdValue = Constants.SSL_TRUSTSTORE_PWD_DEFAULT;
+ }
+
+ File keyStoreFile = new File(sslTrustStoreValue);
+ KeyStore ks = KeyStore.getInstance("JKS");
+ try (InputStream is = new FileInputStream(keyStoreFile)) {
+ ks.load(is, sslTrustStorePwdValue.toCharArray());
+ }
+
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(
+ TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(ks);
+
+ sslContext.init(null, tmf.getTrustManagers(), null);
+ } else {
+ sslContext.init(null, null, null);
+ }
+ }
+
+ SSLEngine engine = sslContext.createSSLEngine(host, port);
+
+ String sslProtocolsValue =
+ (String) userProperties.get(Constants.SSL_PROTOCOLS_PROPERTY);
+ if (sslProtocolsValue != null) {
+ engine.setEnabledProtocols(sslProtocolsValue.split(","));
+ }
+
+ engine.setUseClientMode(true);
+
+ // Enable host verification
+ // Start with current settings (returns a copy)
+ SSLParameters sslParams = engine.getSSLParameters();
+ // Use HTTPS since WebSocket starts over HTTP(S)
+ sslParams.setEndpointIdentificationAlgorithm("HTTPS");
+ // Write the parameters back
+ engine.setSSLParameters(sslParams);
+
+ return engine;
+ } catch (Exception e) {
+ throw new DeploymentException(sm.getString(
+ "wsWebSocketContainer.sslEngineFail"), e);
+ }
+ }
+
+
+ @Override
+ public long getDefaultMaxSessionIdleTimeout() {
+ return defaultMaxSessionIdleTimeout;
+ }
+
+
+ @Override
+ public void setDefaultMaxSessionIdleTimeout(long timeout) {
+ this.defaultMaxSessionIdleTimeout = timeout;
+ }
+
+
+ @Override
+ public int getDefaultMaxBinaryMessageBufferSize() {
+ return maxBinaryMessageBufferSize;
+ }
+
+
+ @Override
+ public void setDefaultMaxBinaryMessageBufferSize(int max) {
+ maxBinaryMessageBufferSize = max;
+ }
+
+
+ @Override
+ public int getDefaultMaxTextMessageBufferSize() {
+ return maxTextMessageBufferSize;
+ }
+
+
+ @Override
+ public void setDefaultMaxTextMessageBufferSize(int max) {
+ maxTextMessageBufferSize = max;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * Currently, this implementation does not support any extensions.
+ */
+ @Override
+ public Set<Extension> getInstalledExtensions() {
+ return Collections.emptySet();
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * The default value for this implementation is -1.
+ */
+ @Override
+ public long getDefaultAsyncSendTimeout() {
+ return defaultAsyncTimeout;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * The default value for this implementation is -1.
+ */
+ @Override
+ public void setAsyncSendTimeout(long timeout) {
+ this.defaultAsyncTimeout = timeout;
+ }
+
+
+ /**
+ * Cleans up the resources still in use by WebSocket sessions created from
+ * this container. This includes closing sessions and cancelling
+ * {@link Future}s associated with blocking read/writes.
+ */
+ public void destroy() {
+ CloseReason cr = new CloseReason(
+ CloseCodes.GOING_AWAY, sm.getString("wsWebSocketContainer.shutdown"));
+
+ for (WsSession session : sessions.keySet()) {
+ try {
+ session.close(cr);
+ } catch (IOException ioe) {
+ log.debug(sm.getString(
+ "wsWebSocketContainer.sessionCloseFail", session.getId()), ioe);
+ }
+ }
+
+ // Only unregister with AsyncChannelGroupUtil if this instance
+ // registered with it
+ if (asynchronousChannelGroup != null) {
+ synchronized (asynchronousChannelGroupLock) {
+ if (asynchronousChannelGroup != null) {
+ AsyncChannelGroupUtil.unregister();
+ asynchronousChannelGroup = null;
+ }
+ }
+ }
+ }
+
+
+ private AsynchronousChannelGroup getAsynchronousChannelGroup() {
+ // Use AsyncChannelGroupUtil to share a common group amongst all
+ // WebSocket clients
+ AsynchronousChannelGroup result = asynchronousChannelGroup;
+ if (result == null) {
+ synchronized (asynchronousChannelGroupLock) {
+ if (asynchronousChannelGroup == null) {
+ asynchronousChannelGroup = AsyncChannelGroupUtil.register();
+ }
+ result = asynchronousChannelGroup;
+ }
+ }
+ return result;
+ }
+
+
+ // ----------------------------------------------- BackgroundProcess methods
+
+ @Override
+ public void backgroundProcess() {
+ // This method gets called once a second.
+ backgroundProcessCount ++;
+ if (backgroundProcessCount >= processPeriod) {
+ backgroundProcessCount = 0;
+
+ for (WsSession wsSession : sessions.keySet()) {
+ wsSession.checkExpiration();
+ }
+ }
+
+ }
+
+
+ @Override
+ public void setProcessPeriod(int period) {
+ this.processPeriod = period;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * The default value is 10 which means session expirations are processed
+ * every 10 seconds.
+ */
+ @Override
+ public int getProcessPeriod() {
+ return processPeriod;
+ }
+
+
+ private static class HttpResponse {
+ private final int status;
+ private final HandshakeResponse handshakeResponse;
+
+ public HttpResponse(int status, HandshakeResponse handshakeResponse) {
+ this.status = status;
+ this.handshakeResponse = handshakeResponse;
+ }
+
+
+ public int getStatus() {
+ return status;
+ }
+
+
+ public HandshakeResponse getHandshakeResponse() {
+ return handshakeResponse;
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/Constants.java b/src/java/nginx/unit/websocket/pojo/Constants.java
new file mode 100644
index 00000000..93cdecc7
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/Constants.java
@@ -0,0 +1,32 @@
+/*
+ * 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 nginx.unit.websocket.pojo;
+
+/**
+ * Internal implementation constants.
+ */
+public class Constants {
+
+ public static final String POJO_PATH_PARAM_KEY =
+ "nginx.unit.websocket.pojo.PojoEndpoint.pathParams";
+ public static final String POJO_METHOD_MAPPING_KEY =
+ "nginx.unit.websocket.pojo.PojoEndpoint.methodMapping";
+
+ private Constants() {
+ // Hide default constructor
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/LocalStrings.properties b/src/java/nginx/unit/websocket/pojo/LocalStrings.properties
new file mode 100644
index 00000000..00ab7e6b
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/LocalStrings.properties
@@ -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.
+
+pojoEndpointBase.closeSessionFail=Failed to close WebSocket session during error handling
+pojoEndpointBase.onCloseFail=Failed to call onClose method of POJO end point for POJO of type [{0}]
+pojoEndpointBase.onError=No error handling configured for [{0}] and the following error occurred
+pojoEndpointBase.onErrorFail=Failed to call onError method of POJO end point for POJO of type [{0}]
+pojoEndpointBase.onOpenFail=Failed to call onOpen method of POJO end point for POJO of type [{0}]
+pojoEndpointServer.getPojoInstanceFail=Failed to create instance of POJO of type [{0}]
+pojoMethodMapping.decodePathParamFail=Failed to decode path parameter value [{0}] to expected type [{1}]
+pojoMethodMapping.duplicateAnnotation=Duplicate annotations [{0}] present on class [{1}]
+pojoMethodMapping.duplicateLastParam=Multiple boolean (last) parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.duplicateMessageParam=Multiple message parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.duplicatePongMessageParam=Multiple PongMessage parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.duplicateSessionParam=Multiple session parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.invalidDecoder=The specified decoder of type [{0}] could not be instantiated
+pojoMethodMapping.invalidPathParamType=Parameters annotated with @PathParam may only be Strings, Java primitives or a boxed version thereof
+pojoMethodMapping.methodNotPublic=The annotated method [{0}] is not public
+pojoMethodMapping.noPayload=No payload parameter present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.onErrorNoThrowable=No Throwable parameter was present on the method [{0}] of class [{1}] that was annotated with OnError
+pojoMethodMapping.paramWithoutAnnotation=A parameter of type [{0}] was found on method[{1}] of class [{2}] that did not have a @PathParam annotation
+pojoMethodMapping.partialInputStream=Invalid InputStream and boolean parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.partialObject=Invalid Object and boolean parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.partialPong=Invalid PongMessage and boolean parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.partialReader=Invalid Reader and boolean parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.pongWithPayload=Invalid PongMessage and Message parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMessageHandlerWhole.decodeIoFail=IO error while decoding message
+pojoMessageHandlerWhole.maxBufferSize=The maximum supported message size for this implementation is Integer.MAX_VALUE
diff --git a/src/java/nginx/unit/websocket/pojo/PojoEndpointBase.java b/src/java/nginx/unit/websocket/pojo/PojoEndpointBase.java
new file mode 100644
index 00000000..be679a35
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoEndpointBase.java
@@ -0,0 +1,156 @@
+/*
+ * 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 nginx.unit.websocket.pojo;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Map;
+import java.util.Set;
+
+import javax.websocket.CloseReason;
+import javax.websocket.Endpoint;
+import javax.websocket.EndpointConfig;
+import javax.websocket.MessageHandler;
+import javax.websocket.Session;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.ExceptionUtils;
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * Base implementation (client and server have different concrete
+ * implementations) of the wrapper that converts a POJO instance into a
+ * WebSocket endpoint instance.
+ */
+public abstract class PojoEndpointBase extends Endpoint {
+
+ private final Log log = LogFactory.getLog(PojoEndpointBase.class); // must not be static
+ private static final StringManager sm = StringManager.getManager(PojoEndpointBase.class);
+
+ private Object pojo;
+ private Map<String,String> pathParameters;
+ private PojoMethodMapping methodMapping;
+
+
+ protected final void doOnOpen(Session session, EndpointConfig config) {
+ PojoMethodMapping methodMapping = getMethodMapping();
+ Object pojo = getPojo();
+ Map<String,String> pathParameters = getPathParameters();
+
+ // Add message handlers before calling onOpen since that may trigger a
+ // message which in turn could trigger a response and/or close the
+ // session
+ for (MessageHandler mh : methodMapping.getMessageHandlers(pojo,
+ pathParameters, session, config)) {
+ session.addMessageHandler(mh);
+ }
+
+ if (methodMapping.getOnOpen() != null) {
+ try {
+ methodMapping.getOnOpen().invoke(pojo,
+ methodMapping.getOnOpenArgs(
+ pathParameters, session, config));
+
+ } catch (IllegalAccessException e) {
+ // Reflection related problems
+ log.error(sm.getString(
+ "pojoEndpointBase.onOpenFail",
+ pojo.getClass().getName()), e);
+ handleOnOpenOrCloseError(session, e);
+ } catch (InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ handleOnOpenOrCloseError(session, cause);
+ } catch (Throwable t) {
+ handleOnOpenOrCloseError(session, t);
+ }
+ }
+ }
+
+
+ private void handleOnOpenOrCloseError(Session session, Throwable t) {
+ // If really fatal - re-throw
+ ExceptionUtils.handleThrowable(t);
+
+ // Trigger the error handler and close the session
+ onError(session, t);
+ try {
+ session.close();
+ } catch (IOException ioe) {
+ log.warn(sm.getString("pojoEndpointBase.closeSessionFail"), ioe);
+ }
+ }
+
+ @Override
+ public final void onClose(Session session, CloseReason closeReason) {
+
+ if (methodMapping.getOnClose() != null) {
+ try {
+ methodMapping.getOnClose().invoke(pojo,
+ methodMapping.getOnCloseArgs(pathParameters, session, closeReason));
+ } catch (Throwable t) {
+ log.error(sm.getString("pojoEndpointBase.onCloseFail",
+ pojo.getClass().getName()), t);
+ handleOnOpenOrCloseError(session, t);
+ }
+ }
+
+ // Trigger the destroy method for any associated decoders
+ Set<MessageHandler> messageHandlers = session.getMessageHandlers();
+ for (MessageHandler messageHandler : messageHandlers) {
+ if (messageHandler instanceof PojoMessageHandlerWholeBase<?>) {
+ ((PojoMessageHandlerWholeBase<?>) messageHandler).onClose();
+ }
+ }
+ }
+
+
+ @Override
+ public final void onError(Session session, Throwable throwable) {
+
+ if (methodMapping.getOnError() == null) {
+ log.error(sm.getString("pojoEndpointBase.onError",
+ pojo.getClass().getName()), throwable);
+ } else {
+ try {
+ methodMapping.getOnError().invoke(
+ pojo,
+ methodMapping.getOnErrorArgs(pathParameters, session,
+ throwable));
+ } catch (Throwable t) {
+ ExceptionUtils.handleThrowable(t);
+ log.error(sm.getString("pojoEndpointBase.onErrorFail",
+ pojo.getClass().getName()), t);
+ }
+ }
+ }
+
+ protected Object getPojo() { return pojo; }
+ protected void setPojo(Object pojo) { this.pojo = pojo; }
+
+
+ protected Map<String,String> getPathParameters() { return pathParameters; }
+ protected void setPathParameters(Map<String,String> pathParameters) {
+ this.pathParameters = pathParameters;
+ }
+
+
+ protected PojoMethodMapping getMethodMapping() { return methodMapping; }
+ protected void setMethodMapping(PojoMethodMapping methodMapping) {
+ this.methodMapping = methodMapping;
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoEndpointClient.java b/src/java/nginx/unit/websocket/pojo/PojoEndpointClient.java
new file mode 100644
index 00000000..6e569487
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoEndpointClient.java
@@ -0,0 +1,47 @@
+/*
+ * 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 nginx.unit.websocket.pojo;
+
+import java.util.Collections;
+import java.util.List;
+
+import javax.websocket.Decoder;
+import javax.websocket.DeploymentException;
+import javax.websocket.EndpointConfig;
+import javax.websocket.Session;
+
+
+/**
+ * Wrapper class for instances of POJOs annotated with
+ * {@link javax.websocket.ClientEndpoint} so they appear as standard
+ * {@link javax.websocket.Endpoint} instances.
+ */
+public class PojoEndpointClient extends PojoEndpointBase {
+
+ public PojoEndpointClient(Object pojo,
+ List<Class<? extends Decoder>> decoders) throws DeploymentException {
+ setPojo(pojo);
+ setMethodMapping(
+ new PojoMethodMapping(pojo.getClass(), decoders, null));
+ setPathParameters(Collections.<String,String>emptyMap());
+ }
+
+ @Override
+ public void onOpen(Session session, EndpointConfig config) {
+ doOnOpen(session, config);
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoEndpointServer.java b/src/java/nginx/unit/websocket/pojo/PojoEndpointServer.java
new file mode 100644
index 00000000..499f8274
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoEndpointServer.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package nginx.unit.websocket.pojo;
+
+import java.util.Map;
+
+import javax.websocket.EndpointConfig;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * Wrapper class for instances of POJOs annotated with
+ * {@link javax.websocket.server.ServerEndpoint} so they appear as standard
+ * {@link javax.websocket.Endpoint} instances.
+ */
+public class PojoEndpointServer extends PojoEndpointBase {
+
+ private static final StringManager sm =
+ StringManager.getManager(PojoEndpointServer.class);
+
+ @Override
+ public void onOpen(Session session, EndpointConfig endpointConfig) {
+
+ ServerEndpointConfig sec = (ServerEndpointConfig) endpointConfig;
+
+ Object pojo;
+ try {
+ pojo = sec.getConfigurator().getEndpointInstance(
+ sec.getEndpointClass());
+ } catch (InstantiationException e) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoEndpointServer.getPojoInstanceFail",
+ sec.getEndpointClass().getName()), e);
+ }
+ setPojo(pojo);
+
+ @SuppressWarnings("unchecked")
+ Map<String,String> pathParameters =
+ (Map<String, String>) sec.getUserProperties().get(
+ Constants.POJO_PATH_PARAM_KEY);
+ setPathParameters(pathParameters);
+
+ PojoMethodMapping methodMapping =
+ (PojoMethodMapping) sec.getUserProperties().get(
+ Constants.POJO_METHOD_MAPPING_KEY);
+ setMethodMapping(methodMapping);
+
+ doOnOpen(session, endpointConfig);
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerBase.java b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerBase.java
new file mode 100644
index 00000000..b72d719a
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerBase.java
@@ -0,0 +1,122 @@
+/*
+ * 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 nginx.unit.websocket.pojo;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+
+import javax.websocket.EncodeException;
+import javax.websocket.MessageHandler;
+import javax.websocket.RemoteEndpoint;
+import javax.websocket.Session;
+
+import org.apache.tomcat.util.ExceptionUtils;
+import nginx.unit.websocket.WrappedMessageHandler;
+
+/**
+ * Common implementation code for the POJO message handlers.
+ *
+ * @param <T> The type of message to handle
+ */
+public abstract class PojoMessageHandlerBase<T>
+ implements WrappedMessageHandler {
+
+ protected final Object pojo;
+ protected final Method method;
+ protected final Session session;
+ protected final Object[] params;
+ protected final int indexPayload;
+ protected final boolean convert;
+ protected final int indexSession;
+ protected final long maxMessageSize;
+
+ public PojoMessageHandlerBase(Object pojo, Method method,
+ Session session, Object[] params, int indexPayload, boolean convert,
+ int indexSession, long maxMessageSize) {
+ this.pojo = pojo;
+ this.method = method;
+ // TODO: The method should already be accessible here but the following
+ // code seems to be necessary in some as yet not fully understood cases.
+ try {
+ this.method.setAccessible(true);
+ } catch (Exception e) {
+ // It is better to make sure the method is accessible, but
+ // ignore exceptions and hope for the best
+ }
+ this.session = session;
+ this.params = params;
+ this.indexPayload = indexPayload;
+ this.convert = convert;
+ this.indexSession = indexSession;
+ this.maxMessageSize = maxMessageSize;
+ }
+
+
+ protected final void processResult(Object result) {
+ if (result == null) {
+ return;
+ }
+
+ RemoteEndpoint.Basic remoteEndpoint = session.getBasicRemote();
+ try {
+ if (result instanceof String) {
+ remoteEndpoint.sendText((String) result);
+ } else if (result instanceof ByteBuffer) {
+ remoteEndpoint.sendBinary((ByteBuffer) result);
+ } else if (result instanceof byte[]) {
+ remoteEndpoint.sendBinary(ByteBuffer.wrap((byte[]) result));
+ } else {
+ remoteEndpoint.sendObject(result);
+ }
+ } catch (IOException | EncodeException ioe) {
+ throw new IllegalStateException(ioe);
+ }
+ }
+
+
+ /**
+ * Expose the POJO if it is a message handler so the Session is able to
+ * match requests to remove handlers if the original handler has been
+ * wrapped.
+ */
+ @Override
+ public final MessageHandler getWrappedHandler() {
+ if (pojo instanceof MessageHandler) {
+ return (MessageHandler) pojo;
+ } else {
+ return null;
+ }
+ }
+
+
+ @Override
+ public final long getMaxMessageSize() {
+ return maxMessageSize;
+ }
+
+
+ protected final void handlePojoMethodException(Throwable t) {
+ t = ExceptionUtils.unwrapInvocationTargetException(t);
+ ExceptionUtils.handleThrowable(t);
+ if (t instanceof RuntimeException) {
+ throw (RuntimeException) t;
+ } else {
+ throw new RuntimeException(t.getMessage(), t);
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBase.java b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBase.java
new file mode 100644
index 00000000..d6f37724
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBase.java
@@ -0,0 +1,77 @@
+/*
+ * 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 nginx.unit.websocket.pojo;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+
+import javax.websocket.DecodeException;
+import javax.websocket.MessageHandler;
+import javax.websocket.Session;
+
+import nginx.unit.websocket.WsSession;
+
+/**
+ * Common implementation code for the POJO partial message handlers. All
+ * the real work is done in this class and in the superclass.
+ *
+ * @param <T> The type of message to handle
+ */
+public abstract class PojoMessageHandlerPartialBase<T>
+ extends PojoMessageHandlerBase<T> implements MessageHandler.Partial<T> {
+
+ private final int indexBoolean;
+
+ public PojoMessageHandlerPartialBase(Object pojo, Method method,
+ Session session, Object[] params, int indexPayload,
+ boolean convert, int indexBoolean, int indexSession,
+ long maxMessageSize) {
+ super(pojo, method, session, params, indexPayload, convert,
+ indexSession, maxMessageSize);
+ this.indexBoolean = indexBoolean;
+ }
+
+
+ @Override
+ public final void onMessage(T message, boolean last) {
+ if (params.length == 1 && params[0] instanceof DecodeException) {
+ ((WsSession) session).getLocal().onError(session,
+ (DecodeException) params[0]);
+ return;
+ }
+ Object[] parameters = params.clone();
+ if (indexBoolean != -1) {
+ parameters[indexBoolean] = Boolean.valueOf(last);
+ }
+ if (indexSession != -1) {
+ parameters[indexSession] = session;
+ }
+ if (convert) {
+ parameters[indexPayload] = ((ByteBuffer) message).array();
+ } else {
+ parameters[indexPayload] = message;
+ }
+ Object result = null;
+ try {
+ result = method.invoke(pojo, parameters);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ handlePojoMethodException(e);
+ }
+ processResult(result);
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBinary.java b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBinary.java
new file mode 100644
index 00000000..1d334017
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBinary.java
@@ -0,0 +1,36 @@
+/*
+ * 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 nginx.unit.websocket.pojo;
+
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+
+import javax.websocket.Session;
+
+/**
+ * ByteBuffer specific concrete implementation for handling partial messages.
+ */
+public class PojoMessageHandlerPartialBinary
+ extends PojoMessageHandlerPartialBase<ByteBuffer> {
+
+ public PojoMessageHandlerPartialBinary(Object pojo, Method method,
+ Session session, Object[] params, int indexPayload, boolean convert,
+ int indexBoolean, int indexSession, long maxMessageSize) {
+ super(pojo, method, session, params, indexPayload, convert, indexBoolean,
+ indexSession, maxMessageSize);
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialText.java b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialText.java
new file mode 100644
index 00000000..8f7c1a0d
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialText.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package nginx.unit.websocket.pojo;
+
+import java.lang.reflect.Method;
+
+import javax.websocket.Session;
+
+/**
+ * Text specific concrete implementation for handling partial messages.
+ */
+public class PojoMessageHandlerPartialText
+ extends PojoMessageHandlerPartialBase<String> {
+
+ public PojoMessageHandlerPartialText(Object pojo, Method method,
+ Session session, Object[] params, int indexPayload, boolean convert,
+ int indexBoolean, int indexSession, long maxMessageSize) {
+ super(pojo, method, session, params, indexPayload, convert, indexBoolean,
+ indexSession, maxMessageSize);
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBase.java b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBase.java
new file mode 100644
index 00000000..23333eb7
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBase.java
@@ -0,0 +1,94 @@
+/*
+ * 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 nginx.unit.websocket.pojo;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import javax.websocket.DecodeException;
+import javax.websocket.MessageHandler;
+import javax.websocket.Session;
+
+import nginx.unit.websocket.WsSession;
+
+/**
+ * Common implementation code for the POJO whole message handlers. All the real
+ * work is done in this class and in the superclass.
+ *
+ * @param <T> The type of message to handle
+ */
+public abstract class PojoMessageHandlerWholeBase<T>
+ extends PojoMessageHandlerBase<T> implements MessageHandler.Whole<T> {
+
+ public PojoMessageHandlerWholeBase(Object pojo, Method method,
+ Session session, Object[] params, int indexPayload,
+ boolean convert, int indexSession, long maxMessageSize) {
+ super(pojo, method, session, params, indexPayload, convert,
+ indexSession, maxMessageSize);
+ }
+
+
+ @Override
+ public final void onMessage(T message) {
+
+ if (params.length == 1 && params[0] instanceof DecodeException) {
+ ((WsSession) session).getLocal().onError(session,
+ (DecodeException) params[0]);
+ return;
+ }
+
+ // Can this message be decoded?
+ Object payload;
+ try {
+ payload = decode(message);
+ } catch (DecodeException de) {
+ ((WsSession) session).getLocal().onError(session, de);
+ return;
+ }
+
+ if (payload == null) {
+ // Not decoded. Convert if required.
+ if (convert) {
+ payload = convert(message);
+ } else {
+ payload = message;
+ }
+ }
+
+ Object[] parameters = params.clone();
+ if (indexSession != -1) {
+ parameters[indexSession] = session;
+ }
+ parameters[indexPayload] = payload;
+
+ Object result = null;
+ try {
+ result = method.invoke(pojo, parameters);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ handlePojoMethodException(e);
+ }
+ processResult(result);
+ }
+
+ protected Object convert(T message) {
+ return message;
+ }
+
+
+ protected abstract Object decode(T message) throws DecodeException;
+ protected abstract void onClose();
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBinary.java b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBinary.java
new file mode 100644
index 00000000..07ff0648
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBinary.java
@@ -0,0 +1,131 @@
+/*
+ * 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 nginx.unit.websocket.pojo;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.Decoder.Binary;
+import javax.websocket.Decoder.BinaryStream;
+import javax.websocket.EndpointConfig;
+import javax.websocket.Session;
+
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * ByteBuffer specific concrete implementation for handling whole messages.
+ */
+public class PojoMessageHandlerWholeBinary
+ extends PojoMessageHandlerWholeBase<ByteBuffer> {
+
+ private static final StringManager sm =
+ StringManager.getManager(PojoMessageHandlerWholeBinary.class);
+
+ private final List<Decoder> decoders = new ArrayList<>();
+
+ private final boolean isForInputStream;
+
+ public PojoMessageHandlerWholeBinary(Object pojo, Method method,
+ Session session, EndpointConfig config,
+ List<Class<? extends Decoder>> decoderClazzes, Object[] params,
+ int indexPayload, boolean convert, int indexSession,
+ boolean isForInputStream, long maxMessageSize) {
+ super(pojo, method, session, params, indexPayload, convert,
+ indexSession, maxMessageSize);
+
+ // Update binary text size handled by session
+ if (maxMessageSize > -1 && maxMessageSize > session.getMaxBinaryMessageBufferSize()) {
+ if (maxMessageSize > Integer.MAX_VALUE) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMessageHandlerWhole.maxBufferSize"));
+ }
+ session.setMaxBinaryMessageBufferSize((int) maxMessageSize);
+ }
+
+ try {
+ if (decoderClazzes != null) {
+ for (Class<? extends Decoder> decoderClazz : decoderClazzes) {
+ if (Binary.class.isAssignableFrom(decoderClazz)) {
+ Binary<?> decoder = (Binary<?>) decoderClazz.getConstructor().newInstance();
+ decoder.init(config);
+ decoders.add(decoder);
+ } else if (BinaryStream.class.isAssignableFrom(
+ decoderClazz)) {
+ BinaryStream<?> decoder = (BinaryStream<?>)
+ decoderClazz.getConstructor().newInstance();
+ decoder.init(config);
+ decoders.add(decoder);
+ } else {
+ // Text decoder - ignore it
+ }
+ }
+ }
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalArgumentException(e);
+ }
+ this.isForInputStream = isForInputStream;
+ }
+
+
+ @Override
+ protected Object decode(ByteBuffer message) throws DecodeException {
+ for (Decoder decoder : decoders) {
+ if (decoder instanceof Binary) {
+ if (((Binary<?>) decoder).willDecode(message)) {
+ return ((Binary<?>) decoder).decode(message);
+ }
+ } else {
+ byte[] array = new byte[message.limit() - message.position()];
+ message.get(array);
+ ByteArrayInputStream bais = new ByteArrayInputStream(array);
+ try {
+ return ((BinaryStream<?>) decoder).decode(bais);
+ } catch (IOException ioe) {
+ throw new DecodeException(message, sm.getString(
+ "pojoMessageHandlerWhole.decodeIoFail"), ioe);
+ }
+ }
+ }
+ return null;
+ }
+
+
+ @Override
+ protected Object convert(ByteBuffer message) {
+ byte[] array = new byte[message.remaining()];
+ message.get(array);
+ if (isForInputStream) {
+ return new ByteArrayInputStream(array);
+ } else {
+ return array;
+ }
+ }
+
+
+ @Override
+ protected void onClose() {
+ for (Decoder decoder : decoders) {
+ decoder.destroy();
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholePong.java b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholePong.java
new file mode 100644
index 00000000..bdedd7de
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholePong.java
@@ -0,0 +1,48 @@
+/*
+ * 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 nginx.unit.websocket.pojo;
+
+import java.lang.reflect.Method;
+
+import javax.websocket.PongMessage;
+import javax.websocket.Session;
+
+/**
+ * PongMessage specific concrete implementation for handling whole messages.
+ */
+public class PojoMessageHandlerWholePong
+ extends PojoMessageHandlerWholeBase<PongMessage> {
+
+ public PojoMessageHandlerWholePong(Object pojo, Method method,
+ Session session, Object[] params, int indexPayload, boolean convert,
+ int indexSession) {
+ super(pojo, method, session, params, indexPayload, convert,
+ indexSession, -1);
+ }
+
+ @Override
+ protected Object decode(PongMessage message) {
+ // Never decoded
+ return null;
+ }
+
+
+ @Override
+ protected void onClose() {
+ // NO-OP
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeText.java b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeText.java
new file mode 100644
index 00000000..59007349
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeText.java
@@ -0,0 +1,136 @@
+/*
+ * 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 nginx.unit.websocket.pojo;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.Decoder.Text;
+import javax.websocket.Decoder.TextStream;
+import javax.websocket.EndpointConfig;
+import javax.websocket.Session;
+
+import org.apache.tomcat.util.res.StringManager;
+import nginx.unit.websocket.Util;
+
+
+/**
+ * Text specific concrete implementation for handling whole messages.
+ */
+public class PojoMessageHandlerWholeText
+ extends PojoMessageHandlerWholeBase<String> {
+
+ private static final StringManager sm =
+ StringManager.getManager(PojoMessageHandlerWholeText.class);
+
+ private final List<Decoder> decoders = new ArrayList<>();
+ private final Class<?> primitiveType;
+
+ public PojoMessageHandlerWholeText(Object pojo, Method method,
+ Session session, EndpointConfig config,
+ List<Class<? extends Decoder>> decoderClazzes, Object[] params,
+ int indexPayload, boolean convert, int indexSession,
+ long maxMessageSize) {
+ super(pojo, method, session, params, indexPayload, convert,
+ indexSession, maxMessageSize);
+
+ // Update max text size handled by session
+ if (maxMessageSize > -1 && maxMessageSize > session.getMaxTextMessageBufferSize()) {
+ if (maxMessageSize > Integer.MAX_VALUE) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMessageHandlerWhole.maxBufferSize"));
+ }
+ session.setMaxTextMessageBufferSize((int) maxMessageSize);
+ }
+
+ // Check for primitives
+ Class<?> type = method.getParameterTypes()[indexPayload];
+ if (Util.isPrimitive(type)) {
+ primitiveType = type;
+ return;
+ } else {
+ primitiveType = null;
+ }
+
+ try {
+ if (decoderClazzes != null) {
+ for (Class<? extends Decoder> decoderClazz : decoderClazzes) {
+ if (Text.class.isAssignableFrom(decoderClazz)) {
+ Text<?> decoder = (Text<?>) decoderClazz.getConstructor().newInstance();
+ decoder.init(config);
+ decoders.add(decoder);
+ } else if (TextStream.class.isAssignableFrom(
+ decoderClazz)) {
+ TextStream<?> decoder =
+ (TextStream<?>) decoderClazz.getConstructor().newInstance();
+ decoder.init(config);
+ decoders.add(decoder);
+ } else {
+ // Binary decoder - ignore it
+ }
+ }
+ }
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+
+ @Override
+ protected Object decode(String message) throws DecodeException {
+ // Handle primitives
+ if (primitiveType != null) {
+ return Util.coerceToType(primitiveType, message);
+ }
+ // Handle full decoders
+ for (Decoder decoder : decoders) {
+ if (decoder instanceof Text) {
+ if (((Text<?>) decoder).willDecode(message)) {
+ return ((Text<?>) decoder).decode(message);
+ }
+ } else {
+ StringReader r = new StringReader(message);
+ try {
+ return ((TextStream<?>) decoder).decode(r);
+ } catch (IOException ioe) {
+ throw new DecodeException(message, sm.getString(
+ "pojoMessageHandlerWhole.decodeIoFail"), ioe);
+ }
+ }
+ }
+ return null;
+ }
+
+
+ @Override
+ protected Object convert(String message) {
+ return new StringReader(message);
+ }
+
+
+ @Override
+ protected void onClose() {
+ for (Decoder decoder : decoders) {
+ decoder.destroy();
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMethodMapping.java b/src/java/nginx/unit/websocket/pojo/PojoMethodMapping.java
new file mode 100644
index 00000000..2385b5c7
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMethodMapping.java
@@ -0,0 +1,731 @@
+/*
+ * 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 nginx.unit.websocket.pojo;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.websocket.CloseReason;
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.DeploymentException;
+import javax.websocket.EndpointConfig;
+import javax.websocket.MessageHandler;
+import javax.websocket.OnClose;
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.PongMessage;
+import javax.websocket.Session;
+import javax.websocket.server.PathParam;
+
+import org.apache.tomcat.util.res.StringManager;
+import nginx.unit.websocket.DecoderEntry;
+import nginx.unit.websocket.Util;
+import nginx.unit.websocket.Util.DecoderMatch;
+
+/**
+ * For a POJO class annotated with
+ * {@link javax.websocket.server.ServerEndpoint}, an instance of this class
+ * creates and caches the method handler, method information and parameter
+ * information for the onXXX calls.
+ */
+public class PojoMethodMapping {
+
+ private static final StringManager sm =
+ StringManager.getManager(PojoMethodMapping.class);
+
+ private final Method onOpen;
+ private final Method onClose;
+ private final Method onError;
+ private final PojoPathParam[] onOpenParams;
+ private final PojoPathParam[] onCloseParams;
+ private final PojoPathParam[] onErrorParams;
+ private final List<MessageHandlerInfo> onMessage = new ArrayList<>();
+ private final String wsPath;
+
+
+ public PojoMethodMapping(Class<?> clazzPojo,
+ List<Class<? extends Decoder>> decoderClazzes, String wsPath)
+ throws DeploymentException {
+
+ this.wsPath = wsPath;
+
+ List<DecoderEntry> decoders = Util.getDecoders(decoderClazzes);
+ Method open = null;
+ Method close = null;
+ Method error = null;
+ Method[] clazzPojoMethods = null;
+ Class<?> currentClazz = clazzPojo;
+ while (!currentClazz.equals(Object.class)) {
+ Method[] currentClazzMethods = currentClazz.getDeclaredMethods();
+ if (currentClazz == clazzPojo) {
+ clazzPojoMethods = currentClazzMethods;
+ }
+ for (Method method : currentClazzMethods) {
+ if (method.getAnnotation(OnOpen.class) != null) {
+ checkPublic(method);
+ if (open == null) {
+ open = method;
+ } else {
+ if (currentClazz == clazzPojo ||
+ !isMethodOverride(open, method)) {
+ // Duplicate annotation
+ throw new DeploymentException(sm.getString(
+ "pojoMethodMapping.duplicateAnnotation",
+ OnOpen.class, currentClazz));
+ }
+ }
+ } else if (method.getAnnotation(OnClose.class) != null) {
+ checkPublic(method);
+ if (close == null) {
+ close = method;
+ } else {
+ if (currentClazz == clazzPojo ||
+ !isMethodOverride(close, method)) {
+ // Duplicate annotation
+ throw new DeploymentException(sm.getString(
+ "pojoMethodMapping.duplicateAnnotation",
+ OnClose.class, currentClazz));
+ }
+ }
+ } else if (method.getAnnotation(OnError.class) != null) {
+ checkPublic(method);
+ if (error == null) {
+ error = method;
+ } else {
+ if (currentClazz == clazzPojo ||
+ !isMethodOverride(error, method)) {
+ // Duplicate annotation
+ throw new DeploymentException(sm.getString(
+ "pojoMethodMapping.duplicateAnnotation",
+ OnError.class, currentClazz));
+ }
+ }
+ } else if (method.getAnnotation(OnMessage.class) != null) {
+ checkPublic(method);
+ MessageHandlerInfo messageHandler = new MessageHandlerInfo(method, decoders);
+ boolean found = false;
+ for (MessageHandlerInfo otherMessageHandler : onMessage) {
+ if (messageHandler.targetsSameWebSocketMessageType(otherMessageHandler)) {
+ found = true;
+ if (currentClazz == clazzPojo ||
+ !isMethodOverride(messageHandler.m, otherMessageHandler.m)) {
+ // Duplicate annotation
+ throw new DeploymentException(sm.getString(
+ "pojoMethodMapping.duplicateAnnotation",
+ OnMessage.class, currentClazz));
+ }
+ }
+ }
+ if (!found) {
+ onMessage.add(messageHandler);
+ }
+ } else {
+ // Method not annotated
+ }
+ }
+ currentClazz = currentClazz.getSuperclass();
+ }
+ // If the methods are not on clazzPojo and they are overridden
+ // by a non annotated method in clazzPojo, they should be ignored
+ if (open != null && open.getDeclaringClass() != clazzPojo) {
+ if (isOverridenWithoutAnnotation(clazzPojoMethods, open, OnOpen.class)) {
+ open = null;
+ }
+ }
+ if (close != null && close.getDeclaringClass() != clazzPojo) {
+ if (isOverridenWithoutAnnotation(clazzPojoMethods, close, OnClose.class)) {
+ close = null;
+ }
+ }
+ if (error != null && error.getDeclaringClass() != clazzPojo) {
+ if (isOverridenWithoutAnnotation(clazzPojoMethods, error, OnError.class)) {
+ error = null;
+ }
+ }
+ List<MessageHandlerInfo> overriddenOnMessage = new ArrayList<>();
+ for (MessageHandlerInfo messageHandler : onMessage) {
+ if (messageHandler.m.getDeclaringClass() != clazzPojo
+ && isOverridenWithoutAnnotation(clazzPojoMethods, messageHandler.m, OnMessage.class)) {
+ overriddenOnMessage.add(messageHandler);
+ }
+ }
+ for (MessageHandlerInfo messageHandler : overriddenOnMessage) {
+ onMessage.remove(messageHandler);
+ }
+ this.onOpen = open;
+ this.onClose = close;
+ this.onError = error;
+ onOpenParams = getPathParams(onOpen, MethodType.ON_OPEN);
+ onCloseParams = getPathParams(onClose, MethodType.ON_CLOSE);
+ onErrorParams = getPathParams(onError, MethodType.ON_ERROR);
+ }
+
+
+ private void checkPublic(Method m) throws DeploymentException {
+ if (!Modifier.isPublic(m.getModifiers())) {
+ throw new DeploymentException(sm.getString(
+ "pojoMethodMapping.methodNotPublic", m.getName()));
+ }
+ }
+
+
+ private boolean isMethodOverride(Method method1, Method method2) {
+ return method1.getName().equals(method2.getName())
+ && method1.getReturnType().equals(method2.getReturnType())
+ && Arrays.equals(method1.getParameterTypes(), method2.getParameterTypes());
+ }
+
+
+ private boolean isOverridenWithoutAnnotation(Method[] methods,
+ Method superclazzMethod, Class<? extends Annotation> annotation) {
+ for (Method method : methods) {
+ if (isMethodOverride(method, superclazzMethod)
+ && (method.getAnnotation(annotation) == null)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ public String getWsPath() {
+ return wsPath;
+ }
+
+
+ public Method getOnOpen() {
+ return onOpen;
+ }
+
+
+ public Object[] getOnOpenArgs(Map<String,String> pathParameters,
+ Session session, EndpointConfig config) throws DecodeException {
+ return buildArgs(onOpenParams, pathParameters, session, config, null,
+ null);
+ }
+
+
+ public Method getOnClose() {
+ return onClose;
+ }
+
+
+ public Object[] getOnCloseArgs(Map<String,String> pathParameters,
+ Session session, CloseReason closeReason) throws DecodeException {
+ return buildArgs(onCloseParams, pathParameters, session, null, null,
+ closeReason);
+ }
+
+
+ public Method getOnError() {
+ return onError;
+ }
+
+
+ public Object[] getOnErrorArgs(Map<String,String> pathParameters,
+ Session session, Throwable throwable) throws DecodeException {
+ return buildArgs(onErrorParams, pathParameters, session, null,
+ throwable, null);
+ }
+
+
+ public boolean hasMessageHandlers() {
+ return !onMessage.isEmpty();
+ }
+
+
+ public Set<MessageHandler> getMessageHandlers(Object pojo,
+ Map<String,String> pathParameters, Session session,
+ EndpointConfig config) {
+ Set<MessageHandler> result = new HashSet<>();
+ for (MessageHandlerInfo messageMethod : onMessage) {
+ result.addAll(messageMethod.getMessageHandlers(pojo, pathParameters,
+ session, config));
+ }
+ return result;
+ }
+
+
+ private static PojoPathParam[] getPathParams(Method m,
+ MethodType methodType) throws DeploymentException {
+ if (m == null) {
+ return new PojoPathParam[0];
+ }
+ boolean foundThrowable = false;
+ Class<?>[] types = m.getParameterTypes();
+ Annotation[][] paramsAnnotations = m.getParameterAnnotations();
+ PojoPathParam[] result = new PojoPathParam[types.length];
+ for (int i = 0; i < types.length; i++) {
+ Class<?> type = types[i];
+ if (type.equals(Session.class)) {
+ result[i] = new PojoPathParam(type, null);
+ } else if (methodType == MethodType.ON_OPEN &&
+ type.equals(EndpointConfig.class)) {
+ result[i] = new PojoPathParam(type, null);
+ } else if (methodType == MethodType.ON_ERROR
+ && type.equals(Throwable.class)) {
+ foundThrowable = true;
+ result[i] = new PojoPathParam(type, null);
+ } else if (methodType == MethodType.ON_CLOSE &&
+ type.equals(CloseReason.class)) {
+ result[i] = new PojoPathParam(type, null);
+ } else {
+ Annotation[] paramAnnotations = paramsAnnotations[i];
+ for (Annotation paramAnnotation : paramAnnotations) {
+ if (paramAnnotation.annotationType().equals(
+ PathParam.class)) {
+ // Check that the type is valid. "0" coerces to every
+ // valid type
+ try {
+ Util.coerceToType(type, "0");
+ } catch (IllegalArgumentException iae) {
+ throw new DeploymentException(sm.getString(
+ "pojoMethodMapping.invalidPathParamType"),
+ iae);
+ }
+ result[i] = new PojoPathParam(type,
+ ((PathParam) paramAnnotation).value());
+ break;
+ }
+ }
+ // Parameters without annotations are not permitted
+ if (result[i] == null) {
+ throw new DeploymentException(sm.getString(
+ "pojoMethodMapping.paramWithoutAnnotation",
+ type, m.getName(), m.getClass().getName()));
+ }
+ }
+ }
+ if (methodType == MethodType.ON_ERROR && !foundThrowable) {
+ throw new DeploymentException(sm.getString(
+ "pojoMethodMapping.onErrorNoThrowable",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ return result;
+ }
+
+
+ private static Object[] buildArgs(PojoPathParam[] pathParams,
+ Map<String,String> pathParameters, Session session,
+ EndpointConfig config, Throwable throwable, CloseReason closeReason)
+ throws DecodeException {
+ Object[] result = new Object[pathParams.length];
+ for (int i = 0; i < pathParams.length; i++) {
+ Class<?> type = pathParams[i].getType();
+ if (type.equals(Session.class)) {
+ result[i] = session;
+ } else if (type.equals(EndpointConfig.class)) {
+ result[i] = config;
+ } else if (type.equals(Throwable.class)) {
+ result[i] = throwable;
+ } else if (type.equals(CloseReason.class)) {
+ result[i] = closeReason;
+ } else {
+ String name = pathParams[i].getName();
+ String value = pathParameters.get(name);
+ try {
+ result[i] = Util.coerceToType(type, value);
+ } catch (Exception e) {
+ throw new DecodeException(value, sm.getString(
+ "pojoMethodMapping.decodePathParamFail",
+ value, type), e);
+ }
+ }
+ }
+ return result;
+ }
+
+
+ private static class MessageHandlerInfo {
+
+ private final Method m;
+ private int indexString = -1;
+ private int indexByteArray = -1;
+ private int indexByteBuffer = -1;
+ private int indexPong = -1;
+ private int indexBoolean = -1;
+ private int indexSession = -1;
+ private int indexInputStream = -1;
+ private int indexReader = -1;
+ private int indexPrimitive = -1;
+ private Class<?> primitiveType = null;
+ private Map<Integer,PojoPathParam> indexPathParams = new HashMap<>();
+ private int indexPayload = -1;
+ private DecoderMatch decoderMatch = null;
+ private long maxMessageSize = -1;
+
+ public MessageHandlerInfo(Method m, List<DecoderEntry> decoderEntries) {
+ this.m = m;
+
+ Class<?>[] types = m.getParameterTypes();
+ Annotation[][] paramsAnnotations = m.getParameterAnnotations();
+
+ for (int i = 0; i < types.length; i++) {
+ boolean paramFound = false;
+ Annotation[] paramAnnotations = paramsAnnotations[i];
+ for (Annotation paramAnnotation : paramAnnotations) {
+ if (paramAnnotation.annotationType().equals(
+ PathParam.class)) {
+ indexPathParams.put(
+ Integer.valueOf(i), new PojoPathParam(types[i],
+ ((PathParam) paramAnnotation).value()));
+ paramFound = true;
+ break;
+ }
+ }
+ if (paramFound) {
+ continue;
+ }
+ if (String.class.isAssignableFrom(types[i])) {
+ if (indexString == -1) {
+ indexString = i;
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else if (Reader.class.isAssignableFrom(types[i])) {
+ if (indexReader == -1) {
+ indexReader = i;
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else if (boolean.class == types[i]) {
+ if (indexBoolean == -1) {
+ indexBoolean = i;
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateLastParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else if (ByteBuffer.class.isAssignableFrom(types[i])) {
+ if (indexByteBuffer == -1) {
+ indexByteBuffer = i;
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else if (byte[].class == types[i]) {
+ if (indexByteArray == -1) {
+ indexByteArray = i;
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else if (InputStream.class.isAssignableFrom(types[i])) {
+ if (indexInputStream == -1) {
+ indexInputStream = i;
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else if (Util.isPrimitive(types[i])) {
+ if (indexPrimitive == -1) {
+ indexPrimitive = i;
+ primitiveType = types[i];
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else if (Session.class.isAssignableFrom(types[i])) {
+ if (indexSession == -1) {
+ indexSession = i;
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateSessionParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else if (PongMessage.class.isAssignableFrom(types[i])) {
+ if (indexPong == -1) {
+ indexPong = i;
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicatePongMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else {
+ if (decoderMatch != null && decoderMatch.hasMatches()) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ decoderMatch = new DecoderMatch(types[i], decoderEntries);
+
+ if (decoderMatch.hasMatches()) {
+ indexPayload = i;
+ }
+ }
+ }
+
+ // Additional checks required
+ if (indexString != -1) {
+ if (indexPayload != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ } else {
+ indexPayload = indexString;
+ }
+ }
+ if (indexReader != -1) {
+ if (indexPayload != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ } else {
+ indexPayload = indexReader;
+ }
+ }
+ if (indexByteArray != -1) {
+ if (indexPayload != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ } else {
+ indexPayload = indexByteArray;
+ }
+ }
+ if (indexByteBuffer != -1) {
+ if (indexPayload != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ } else {
+ indexPayload = indexByteBuffer;
+ }
+ }
+ if (indexInputStream != -1) {
+ if (indexPayload != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ } else {
+ indexPayload = indexInputStream;
+ }
+ }
+ if (indexPrimitive != -1) {
+ if (indexPayload != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ } else {
+ indexPayload = indexPrimitive;
+ }
+ }
+ if (indexPong != -1) {
+ if (indexPayload != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.pongWithPayload",
+ m.getName(), m.getDeclaringClass().getName()));
+ } else {
+ indexPayload = indexPong;
+ }
+ }
+ if (indexPayload == -1 && indexPrimitive == -1 &&
+ indexBoolean != -1) {
+ // The boolean we found is a payload, not a last flag
+ indexPayload = indexBoolean;
+ indexPrimitive = indexBoolean;
+ primitiveType = Boolean.TYPE;
+ indexBoolean = -1;
+ }
+ if (indexPayload == -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.noPayload",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ if (indexPong != -1 && indexBoolean != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.partialPong",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ if(indexReader != -1 && indexBoolean != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.partialReader",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ if(indexInputStream != -1 && indexBoolean != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.partialInputStream",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ if (decoderMatch != null && decoderMatch.hasMatches() &&
+ indexBoolean != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.partialObject",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+
+ maxMessageSize = m.getAnnotation(OnMessage.class).maxMessageSize();
+ }
+
+
+ public boolean targetsSameWebSocketMessageType(MessageHandlerInfo otherHandler) {
+ if (otherHandler == null) {
+ return false;
+ }
+ if (indexByteArray >= 0 && otherHandler.indexByteArray >= 0) {
+ return true;
+ }
+ if (indexByteBuffer >= 0 && otherHandler.indexByteBuffer >= 0) {
+ return true;
+ }
+ if (indexInputStream >= 0 && otherHandler.indexInputStream >= 0) {
+ return true;
+ }
+ if (indexPong >= 0 && otherHandler.indexPong >= 0) {
+ return true;
+ }
+ if (indexPrimitive >= 0 && otherHandler.indexPrimitive >= 0
+ && primitiveType == otherHandler.primitiveType) {
+ return true;
+ }
+ if (indexReader >= 0 && otherHandler.indexReader >= 0) {
+ return true;
+ }
+ if (indexString >= 0 && otherHandler.indexString >= 0) {
+ return true;
+ }
+ if (decoderMatch != null && otherHandler.decoderMatch != null
+ && decoderMatch.getTarget().equals(otherHandler.decoderMatch.getTarget())) {
+ return true;
+ }
+ return false;
+ }
+
+
+ public Set<MessageHandler> getMessageHandlers(Object pojo,
+ Map<String,String> pathParameters, Session session,
+ EndpointConfig config) {
+ Object[] params = new Object[m.getParameterTypes().length];
+
+ for (Map.Entry<Integer,PojoPathParam> entry :
+ indexPathParams.entrySet()) {
+ PojoPathParam pathParam = entry.getValue();
+ String valueString = pathParameters.get(pathParam.getName());
+ Object value = null;
+ try {
+ value = Util.coerceToType(pathParam.getType(), valueString);
+ } catch (Exception e) {
+ DecodeException de = new DecodeException(valueString,
+ sm.getString(
+ "pojoMethodMapping.decodePathParamFail",
+ valueString, pathParam.getType()), e);
+ params = new Object[] { de };
+ break;
+ }
+ params[entry.getKey().intValue()] = value;
+ }
+
+ Set<MessageHandler> results = new HashSet<>(2);
+ if (indexBoolean == -1) {
+ // Basic
+ if (indexString != -1 || indexPrimitive != -1) {
+ MessageHandler mh = new PojoMessageHandlerWholeText(pojo, m,
+ session, config, null, params, indexPayload, false,
+ indexSession, maxMessageSize);
+ results.add(mh);
+ } else if (indexReader != -1) {
+ MessageHandler mh = new PojoMessageHandlerWholeText(pojo, m,
+ session, config, null, params, indexReader, true,
+ indexSession, maxMessageSize);
+ results.add(mh);
+ } else if (indexByteArray != -1) {
+ MessageHandler mh = new PojoMessageHandlerWholeBinary(pojo,
+ m, session, config, null, params, indexByteArray,
+ true, indexSession, false, maxMessageSize);
+ results.add(mh);
+ } else if (indexByteBuffer != -1) {
+ MessageHandler mh = new PojoMessageHandlerWholeBinary(pojo,
+ m, session, config, null, params, indexByteBuffer,
+ false, indexSession, false, maxMessageSize);
+ results.add(mh);
+ } else if (indexInputStream != -1) {
+ MessageHandler mh = new PojoMessageHandlerWholeBinary(pojo,
+ m, session, config, null, params, indexInputStream,
+ true, indexSession, true, maxMessageSize);
+ results.add(mh);
+ } else if (decoderMatch != null && decoderMatch.hasMatches()) {
+ if (decoderMatch.getBinaryDecoders().size() > 0) {
+ MessageHandler mh = new PojoMessageHandlerWholeBinary(
+ pojo, m, session, config,
+ decoderMatch.getBinaryDecoders(), params,
+ indexPayload, true, indexSession, true,
+ maxMessageSize);
+ results.add(mh);
+ }
+ if (decoderMatch.getTextDecoders().size() > 0) {
+ MessageHandler mh = new PojoMessageHandlerWholeText(
+ pojo, m, session, config,
+ decoderMatch.getTextDecoders(), params,
+ indexPayload, true, indexSession, maxMessageSize);
+ results.add(mh);
+ }
+ } else {
+ MessageHandler mh = new PojoMessageHandlerWholePong(pojo, m,
+ session, params, indexPong, false, indexSession);
+ results.add(mh);
+ }
+ } else {
+ // ASync
+ if (indexString != -1) {
+ MessageHandler mh = new PojoMessageHandlerPartialText(pojo,
+ m, session, params, indexString, false,
+ indexBoolean, indexSession, maxMessageSize);
+ results.add(mh);
+ } else if (indexByteArray != -1) {
+ MessageHandler mh = new PojoMessageHandlerPartialBinary(
+ pojo, m, session, params, indexByteArray, true,
+ indexBoolean, indexSession, maxMessageSize);
+ results.add(mh);
+ } else {
+ MessageHandler mh = new PojoMessageHandlerPartialBinary(
+ pojo, m, session, params, indexByteBuffer, false,
+ indexBoolean, indexSession, maxMessageSize);
+ results.add(mh);
+ }
+ }
+ return results;
+ }
+ }
+
+
+ private enum MethodType {
+ ON_OPEN,
+ ON_CLOSE,
+ ON_ERROR
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoPathParam.java b/src/java/nginx/unit/websocket/pojo/PojoPathParam.java
new file mode 100644
index 00000000..859b6d68
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoPathParam.java
@@ -0,0 +1,47 @@
+/*
+ * 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 nginx.unit.websocket.pojo;
+
+/**
+ * Stores the parameter type and name for a parameter that needs to be passed to
+ * an onXxx method of {@link javax.websocket.Endpoint}. The name is only present
+ * for parameters annotated with
+ * {@link javax.websocket.server.PathParam}. For the
+ * {@link javax.websocket.Session} and {@link java.lang.Throwable} parameters,
+ * {@link #getName()} will always return <code>null</code>.
+ */
+public class PojoPathParam {
+
+ private final Class<?> type;
+ private final String name;
+
+
+ public PojoPathParam(Class<?> type, String name) {
+ this.type = type;
+ this.name = name;
+ }
+
+
+ public Class<?> getType() {
+ return type;
+ }
+
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/package-info.java b/src/java/nginx/unit/websocket/pojo/package-info.java
new file mode 100644
index 00000000..39cf80c8
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/package-info.java
@@ -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.
+ */
+/**
+ * This package provides the necessary plumbing to convert an annotated POJO
+ * into a WebSocket {@link javax.websocket.Endpoint}.
+ */
+package nginx.unit.websocket.pojo;
diff --git a/src/java/nginx/unit/websocket/server/Constants.java b/src/java/nginx/unit/websocket/server/Constants.java
new file mode 100644
index 00000000..5210c4ba
--- /dev/null
+++ b/src/java/nginx/unit/websocket/server/Constants.java
@@ -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 nginx.unit.websocket.server;
+
+/**
+ * Internal implementation constants.
+ */
+public class Constants {
+
+ public static final String BINARY_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM =
+ "nginx.unit.websocket.binaryBufferSize";
+ public static final String TEXT_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM =
+ "nginx.unit.websocket.textBufferSize";
+ public static final String ENFORCE_NO_ADD_AFTER_HANDSHAKE_CONTEXT_INIT_PARAM =
+ "nginx.unit.websocket.noAddAfterHandshake";
+
+ public static final String SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE =
+ "javax.websocket.server.ServerContainer";
+
+
+ private Constants() {
+ // Hide default constructor
+ }
+}
diff --git a/src/java/nginx/unit/websocket/server/DefaultServerEndpointConfigurator.java b/src/java/nginx/unit/websocket/server/DefaultServerEndpointConfigurator.java
new file mode 100644
index 00000000..43ffe2bc
--- /dev/null
+++ b/src/java/nginx/unit/websocket/server/DefaultServerEndpointConfigurator.java
@@ -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 nginx.unit.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
+ }
+
+}
diff --git a/src/java/nginx/unit/websocket/server/LocalStrings.properties b/src/java/nginx/unit/websocket/server/LocalStrings.properties
new file mode 100644
index 00000000..5bc12501
--- /dev/null
+++ b/src/java/nginx/unit/websocket/server/LocalStrings.properties
@@ -0,0 +1,43 @@
+# 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.endpointDeploy=Endpoint class [{0}] deploying to path [{1}] in ServletContext [{2}]
+serverContainer.missingAnnotation=Cannot deploy POJO class [{0}] as it is not annotated with @ServerEndpoint
+serverContainer.missingEndpoint=An Endpoint instance has been request for path [{0}] but no matching Endpoint class was found
+serverContainer.pojoDeploy=POJO class [{0}] deploying to path [{1}] in ServletContext [{2}]
+serverContainer.servletContextMismatch=Attempted to register a POJO annotated for WebSocket at path [{0}] in the ServletContext with context path [{1}] when the WebSocket ServerContainer is allocated to the ServletContext with context path [{2}]
+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
diff --git a/src/java/nginx/unit/websocket/server/UpgradeUtil.java b/src/java/nginx/unit/websocket/server/UpgradeUtil.java
new file mode 100644
index 00000000..162f01c7
--- /dev/null
+++ b/src/java/nginx/unit/websocket/server/UpgradeUtil.java
@@ -0,0 +1,285 @@
+/*
+ * 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 nginx.unit.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 nginx.unit.Request;
+
+import org.apache.tomcat.util.codec.binary.Base64;
+import org.apache.tomcat.util.res.StringManager;
+import org.apache.tomcat.util.security.ConcurrentMessageDigest;
+import nginx.unit.websocket.Constants;
+import nginx.unit.websocket.Transformation;
+import nginx.unit.websocket.TransformationFactory;
+import nginx.unit.websocket.Util;
+import nginx.unit.websocket.WsHandshakeResponse;
+import nginx.unit.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 a HTTP Upgrade request
+ * for the WebSocket protocol, otherwise <code>false</code>
+ */
+ public static boolean isWebSocketUpgradeRequest(ServletRequest request,
+ ServletResponse response) {
+
+ Request r = (Request) request.getAttribute(Request.BARE);
+
+ return ((request instanceof HttpServletRequest) &&
+ (response instanceof HttpServletResponse) &&
+ (r != null) &&
+ (r.isUpgrade()));
+ }
+
+
+ public static void doUpgrade(WsServerContainer sc, HttpServletRequest req,
+ HttpServletResponse resp, ServerEndpointConfig sec,
+ Map<String,String> pathParams)
+ throws ServletException, IOException {
+
+
+ // 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);
+ String 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());
+ }
+ }
+
+ WsHttpUpgradeHandler wsHandler =
+ req.upgrade(WsHttpUpgradeHandler.class);
+
+ 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(
+ nginx.unit.websocket.pojo.Constants.POJO_PATH_PARAM_KEY, pathParams);
+ }
+ } catch (InstantiationException e) {
+ throw new ServletException(e);
+ }
+
+ wsHandler.preInit(ep, perSessionServerEndpointConfig, sc, wsRequest,
+ negotiatedExtensionsPhase2, subProtocol, null, pathParams,
+ req.isSecure());
+
+ wsHandler.init(null);
+ }
+
+
+ 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);
+ }
+}
diff --git a/src/java/nginx/unit/websocket/server/UriTemplate.java b/src/java/nginx/unit/websocket/server/UriTemplate.java
new file mode 100644
index 00000000..7877fac9
--- /dev/null
+++ b/src/java/nginx/unit/websocket/server/UriTemplate.java
@@ -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 nginx.unit.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;
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/server/WsContextListener.java b/src/java/nginx/unit/websocket/server/WsContextListener.java
new file mode 100644
index 00000000..07137856
--- /dev/null
+++ b/src/java/nginx/unit/websocket/server/WsContextListener.java
@@ -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 nginx.unit.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();
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/server/WsFilter.java b/src/java/nginx/unit/websocket/server/WsFilter.java
new file mode 100644
index 00000000..abea71fc
--- /dev/null
+++ b/src/java/nginx/unit/websocket/server/WsFilter.java
@@ -0,0 +1,81 @@
+/*
+ * 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 nginx.unit.websocket.server;
+
+import java.io.IOException;
+
+import javax.servlet.FilterChain;
+import javax.servlet.GenericFilter;
+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 extends GenericFilter {
+
+ private static final long serialVersionUID = 1L;
+
+ private transient WsServerContainer sc;
+
+
+ @Override
+ public void init() throws ServletException {
+ sc = (WsServerContainer) 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());
+ }
+}
diff --git a/src/java/nginx/unit/websocket/server/WsHandshakeRequest.java b/src/java/nginx/unit/websocket/server/WsHandshakeRequest.java
new file mode 100644
index 00000000..fa774302
--- /dev/null
+++ b/src/java/nginx/unit/websocket/server/WsHandshakeRequest.java
@@ -0,0 +1,196 @@
+/*
+ * 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 nginx.unit.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.unmodifiableList(
+ 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);
+ }
+ }
+
+ public Object getAttribute(String name)
+ {
+ return request != null ? request.getAttribute(name) : null;
+ }
+}
diff --git a/src/java/nginx/unit/websocket/server/WsHttpUpgradeHandler.java b/src/java/nginx/unit/websocket/server/WsHttpUpgradeHandler.java
new file mode 100644
index 00000000..cc39ab73
--- /dev/null
+++ b/src/java/nginx/unit/websocket/server/WsHttpUpgradeHandler.java
@@ -0,0 +1,172 @@
+/*
+ * 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 nginx.unit.websocket.server;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpUpgradeHandler;
+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.EndpointConfig;
+import javax.websocket.Extension;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.res.StringManager;
+
+import nginx.unit.websocket.Transformation;
+import nginx.unit.websocket.WsIOException;
+import nginx.unit.websocket.WsSession;
+
+import nginx.unit.Request;
+
+/**
+ * Servlet 3.1 HTTP upgrade handler for WebSocket connections.
+ */
+public class WsHttpUpgradeHandler implements HttpUpgradeHandler {
+
+ 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 Endpoint ep;
+ private EndpointConfig endpointConfig;
+ 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 WsSession wsSession;
+
+
+ public WsHttpUpgradeHandler() {
+ applicationClassLoader = Thread.currentThread().getContextClassLoader();
+ }
+
+ public void preInit(Endpoint ep, EndpointConfig endpointConfig,
+ WsServerContainer wsc, WsHandshakeRequest handshakeRequest,
+ List<Extension> negotiatedExtensionsPhase2, String subProtocol,
+ Transformation transformation, Map<String,String> pathParameters,
+ boolean secure) {
+ this.ep = ep;
+ this.endpointConfig = endpointConfig;
+ 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();
+ }
+
+ nginx.unit.Context.trace("UpgradeHandler.init(" + connection + ")");
+
+/*
+ // 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 {
+ Request r = (Request) handshakeRequest.getAttribute(Request.BARE);
+
+ wsRemoteEndpointServer = new WsRemoteEndpointImplServer(webSocketContainer);
+ wsSession = new WsSession(ep, wsRemoteEndpointServer,
+ webSocketContainer, handshakeRequest.getRequestURI(),
+ handshakeRequest.getParameterMap(),
+ handshakeRequest.getQueryString(),
+ handshakeRequest.getUserPrincipal(), httpSessionId,
+ negotiatedExtensions, subProtocol, pathParameters, secure,
+ endpointConfig, r);
+
+ ep.onOpen(wsSession, endpointConfig);
+ webSocketContainer.registerSession(ep, wsSession);
+ } catch (DeploymentException e) {
+ throw new IllegalArgumentException(e);
+/*
+ } finally {
+ t.setContextClassLoader(cl);
+*/
+ }
+ }
+
+
+
+ @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);
+ }
+}
diff --git a/src/java/nginx/unit/websocket/server/WsMappingResult.java b/src/java/nginx/unit/websocket/server/WsMappingResult.java
new file mode 100644
index 00000000..a7a4c022
--- /dev/null
+++ b/src/java/nginx/unit/websocket/server/WsMappingResult.java
@@ -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 nginx.unit.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;
+ }
+}
diff --git a/src/java/nginx/unit/websocket/server/WsPerSessionServerEndpointConfig.java b/src/java/nginx/unit/websocket/server/WsPerSessionServerEndpointConfig.java
new file mode 100644
index 00000000..2be050cb
--- /dev/null
+++ b/src/java/nginx/unit/websocket/server/WsPerSessionServerEndpointConfig.java
@@ -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 nginx.unit.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();
+ }
+}
diff --git a/src/java/nginx/unit/websocket/server/WsRemoteEndpointImplServer.java b/src/java/nginx/unit/websocket/server/WsRemoteEndpointImplServer.java
new file mode 100644
index 00000000..6d10a3be
--- /dev/null
+++ b/src/java/nginx/unit/websocket/server/WsRemoteEndpointImplServer.java
@@ -0,0 +1,158 @@
+/*
+ * 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 nginx.unit.websocket.server;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.nio.ByteBuffer;
+import java.nio.channels.CompletionHandler;
+import java.nio.channels.InterruptedByTimeoutException;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+
+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.res.StringManager;
+import nginx.unit.websocket.Transformation;
+import nginx.unit.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 volatile SendHandler handler = null;
+ private volatile ByteBuffer[] buffers = null;
+
+ private volatile long timeoutExpiry = -1;
+ private volatile boolean close;
+
+ public WsRemoteEndpointImplServer(
+ WsServerContainer serverContainer) {
+ }
+
+
+ @Override
+ protected final boolean isMasked() {
+ return false;
+ }
+
+ @Override
+ protected void doWrite(SendHandler handler, long blockingWriteTimeoutExpiry,
+ ByteBuffer... buffers) {
+ }
+
+ @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);
+ }
+ }
+
+
+ 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);
+ } 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));
+ }
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/server/WsSci.java b/src/java/nginx/unit/websocket/server/WsSci.java
new file mode 100644
index 00000000..cdecce27
--- /dev/null
+++ b/src/java/nginx/unit/websocket/server/WsSci.java
@@ -0,0 +1,145 @@
+/*
+ * 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 nginx.unit.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;
+
+/**
+ * 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) {
+ int modifiers = clazz.getModifiers();
+ if (!Modifier.isPublic(modifiers) ||
+ Modifier.isAbstract(modifiers)) {
+ // Non-public or abstract - 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);
+ }
+ } 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;
+ }
+}
diff --git a/src/java/nginx/unit/websocket/server/WsServerContainer.java b/src/java/nginx/unit/websocket/server/WsServerContainer.java
new file mode 100644
index 00000000..069fc54f
--- /dev/null
+++ b/src/java/nginx/unit/websocket/server/WsServerContainer.java
@@ -0,0 +1,470 @@
+/*
+ * 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 nginx.unit.websocket.server;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+
+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.Endpoint;
+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 nginx.unit.websocket.WsSession;
+import nginx.unit.websocket.WsWebSocketContainer;
+import nginx.unit.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 ServletContext servletContext;
+ private final Map<String,ServerEndpointConfig> configExactMatchMap =
+ new ConcurrentHashMap<>();
+ private final Map<Integer,SortedSet<TemplatePathMatch>> configTemplateMatchMap =
+ new ConcurrentHashMap<>();
+ private volatile boolean enforceNoAddAfterHandshake =
+ nginx.unit.websocket.Constants.STRICT_SPEC_COMPLIANCE;
+ private volatile boolean addAllowed = true;
+ private final Map<String,Set<WsSession>> authenticatedSessions = new ConcurrentHashMap<>();
+ private volatile boolean endpointsRegistered = 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 {
+
+ if (enforceNoAddAfterHandshake && !addAllowed) {
+ throw new DeploymentException(
+ sm.getString("serverContainer.addNotAllowed"));
+ }
+
+ if (servletContext == null) {
+ throw new DeploymentException(
+ sm.getString("serverContainer.servletContextMissing"));
+ }
+ 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(nginx.unit.websocket.pojo.Constants.POJO_METHOD_MAPPING_KEY,
+ methodMapping);
+ }
+
+ UriTemplate uriTemplate = new UriTemplate(path);
+ if (uriTemplate.hasParameters()) {
+ Integer key = Integer.valueOf(uriTemplate.getSegmentCount());
+ SortedSet<TemplatePathMatch> templateMatches =
+ configTemplateMatchMap.get(key);
+ if (templateMatches == null) {
+ // Ensure that if concurrent threads execute this block they
+ // both end up using the same TreeSet instance
+ templateMatches = new TreeSet<>(
+ TemplatePathMatchComparator.getInstance());
+ configTemplateMatchMap.putIfAbsent(key, templateMatches);
+ templateMatches = configTemplateMatchMap.get(key);
+ }
+ if (!templateMatches.add(new TemplatePathMatch(sec, uriTemplate))) {
+ // Duplicate uriTemplate;
+ throw new DeploymentException(
+ sm.getString("serverContainer.duplicatePaths", path,
+ sec.getEndpointClass(),
+ sec.getEndpointClass()));
+ }
+ } else {
+ // Exact match
+ ServerEndpointConfig old = configExactMatchMap.put(path, sec);
+ if (old != null) {
+ // Duplicate path mappings
+ throw new DeploymentException(
+ sm.getString("serverContainer.duplicatePaths", path,
+ old.getEndpointClass(),
+ sec.getEndpointClass()));
+ }
+ }
+
+ endpointsRegistered = true;
+ }
+
+
+ /**
+ * 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 {
+
+ 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
+ ServerEndpointConfig sec;
+ 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);
+ }
+ }
+ if (configurator == null) {
+ configurator = new nginx.unit.websocket.server.DefaultServerEndpointConfigurator();
+ }
+ sec = ServerEndpointConfig.Builder.create(pojo, path).
+ decoders(Arrays.asList(annotation.decoders())).
+ encoders(Arrays.asList(annotation.encoders())).
+ subprotocols(Arrays.asList(annotation.subprotocols())).
+ configurator(configurator).
+ build();
+
+ addEndpoint(sec);
+ }
+
+
+ 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.
+ ServerEndpointConfig sec = configExactMatchMap.get(path);
+ if (sec != null) {
+ return new WsMappingResult(sec, 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());
+ SortedSet<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.
+ Map<String,String> pathParams = null;
+ for (TemplatePathMatch templateMatch : templateMatches) {
+ 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;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * Overridden to make it visible to other classes in this package.
+ */
+ @Override
+ protected void registerSession(Endpoint endpoint, WsSession wsSession) {
+ super.registerSession(endpoint, 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(Endpoint endpoint, WsSession wsSession) {
+ if (wsSession.getUserPrincipal() != null &&
+ wsSession.getHttpSessionId() != null) {
+ unregisterAuthenticatedSession(wsSession,
+ wsSession.getHttpSessionId());
+ }
+ super.unregisterSession(endpoint, 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;
+
+ public TemplatePathMatch(ServerEndpointConfig config,
+ UriTemplate uriTemplate) {
+ this.config = config;
+ this.uriTemplate = uriTemplate;
+ }
+
+
+ public ServerEndpointConfig getConfig() {
+ return config;
+ }
+
+
+ public UriTemplate getUriTemplate() {
+ return uriTemplate;
+ }
+ }
+
+
+ /**
+ * This Comparator implementation is thread-safe so only create a single
+ * instance.
+ */
+ private static class TemplatePathMatchComparator
+ implements Comparator<TemplatePathMatch> {
+
+ private static final TemplatePathMatchComparator INSTANCE =
+ new TemplatePathMatchComparator();
+
+ public static TemplatePathMatchComparator getInstance() {
+ return INSTANCE;
+ }
+
+ private TemplatePathMatchComparator() {
+ // Hide default constructor
+ }
+
+ @Override
+ public int compare(TemplatePathMatch tpm1, TemplatePathMatch tpm2) {
+ return tpm1.getUriTemplate().getNormalizedPath().compareTo(
+ tpm2.getUriTemplate().getNormalizedPath());
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/server/WsSessionListener.java b/src/java/nginx/unit/websocket/server/WsSessionListener.java
new file mode 100644
index 00000000..fc2bc9c5
--- /dev/null
+++ b/src/java/nginx/unit/websocket/server/WsSessionListener.java
@@ -0,0 +1,36 @@
+/*
+ * 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 nginx.unit.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 sessionDestroyed(HttpSessionEvent se) {
+ wsServerContainer.closeAuthenticatedSession(se.getSession().getId());
+ }
+}
diff --git a/src/java/nginx/unit/websocket/server/WsWriteTimeout.java b/src/java/nginx/unit/websocket/server/WsWriteTimeout.java
new file mode 100644
index 00000000..2dfc4ab2
--- /dev/null
+++ b/src/java/nginx/unit/websocket/server/WsWriteTimeout.java
@@ -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 nginx.unit.websocket.server;
+
+import java.util.Comparator;
+import java.util.Set;
+import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import nginx.unit.websocket.BackgroundProcess;
+import nginx.unit.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;
+ }
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/server/package-info.java b/src/java/nginx/unit/websocket/server/package-info.java
new file mode 100644
index 00000000..87bc85a3
--- /dev/null
+++ b/src/java/nginx/unit/websocket/server/package-info.java
@@ -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 nginx.unit.websocket.server;
diff --git a/src/java/nxt_jni_Request.c b/src/java/nxt_jni_Request.c
index 733290dd..2e9dce67 100644
--- a/src/java/nxt_jni_Request.c
+++ b/src/java/nxt_jni_Request.c
@@ -58,16 +58,28 @@ static jint JNICALL nxt_java_Request_getServerPort(JNIEnv *env, jclass cls,
jlong req_ptr);
static jboolean JNICALL nxt_java_Request_isSecure(JNIEnv *env, jclass cls,
jlong req_ptr);
+static void JNICALL nxt_java_Request_upgrade(JNIEnv *env, jclass cls,
+ jlong req_info_ptr);
+static jboolean JNICALL nxt_java_Request_isUpgrade(JNIEnv *env, jclass cls,
+ jlong req_info_ptr);
static void JNICALL nxt_java_Request_log(JNIEnv *env, jclass cls,
jlong req_info_ptr, jstring msg, jint msg_len);
static void JNICALL nxt_java_Request_trace(JNIEnv *env, jclass cls,
jlong req_info_ptr, jstring msg, jint msg_len);
static jobject JNICALL nxt_java_Request_getResponse(JNIEnv *env, jclass cls,
jlong req_info_ptr);
+static void JNICALL nxt_java_Request_sendWsFrameBuf(JNIEnv *env, jclass cls,
+ jlong req_info_ptr, jobject buf, jint pos, jint len, jbyte opCode, jboolean last);
+static void JNICALL nxt_java_Request_sendWsFrameArr(JNIEnv *env, jclass cls,
+ jlong req_info_ptr, jarray arr, jint pos, jint len, jbyte opCode, jboolean last);
+static void JNICALL nxt_java_Request_closeWs(JNIEnv *env, jclass cls,
+ jlong req_info_ptr);
static jclass nxt_java_Request_class;
static jmethodID nxt_java_Request_ctor;
+static jmethodID nxt_java_Request_processWsFrame;
+static jmethodID nxt_java_Request_closeWsSession;
int
@@ -91,6 +103,18 @@ nxt_java_initRequest(JNIEnv *env, jobject cl)
return NXT_UNIT_ERROR;
}
+ nxt_java_Request_processWsFrame = (*env)->GetMethodID(env, cls, "processWsFrame", "(Ljava/nio/ByteBuffer;BZ)V");
+ if (nxt_java_Request_processWsFrame == NULL) {
+ (*env)->DeleteGlobalRef(env, cls);
+ return NXT_UNIT_ERROR;
+ }
+
+ nxt_java_Request_closeWsSession = (*env)->GetMethodID(env, cls, "closeWsSession", "()V");
+ if (nxt_java_Request_closeWsSession == NULL) {
+ (*env)->DeleteGlobalRef(env, cls);
+ return NXT_UNIT_ERROR;
+ }
+
JNINativeMethod request_methods[] = {
{ (char *) "getHeader",
(char *) "(JLjava/lang/String;I)Ljava/lang/String;",
@@ -172,6 +196,14 @@ nxt_java_initRequest(JNIEnv *env, jobject cl)
(char *) "(J)Z",
nxt_java_Request_isSecure },
+ { (char *) "upgrade",
+ (char *) "(J)V",
+ nxt_java_Request_upgrade },
+
+ { (char *) "isUpgrade",
+ (char *) "(J)Z",
+ nxt_java_Request_isUpgrade },
+
{ (char *) "log",
(char *) "(JLjava/lang/String;I)V",
nxt_java_Request_log },
@@ -184,6 +216,18 @@ nxt_java_initRequest(JNIEnv *env, jobject cl)
(char *) "(J)Lnginx/unit/Response;",
nxt_java_Request_getResponse },
+ { (char *) "sendWsFrame",
+ (char *) "(JLjava/nio/ByteBuffer;IIBZ)V",
+ nxt_java_Request_sendWsFrameBuf },
+
+ { (char *) "sendWsFrame",
+ (char *) "(J[BIIBZ)V",
+ nxt_java_Request_sendWsFrameArr },
+
+ { (char *) "closeWs",
+ (char *) "(J)V",
+ nxt_java_Request_closeWs },
+
};
res = (*env)->RegisterNatives(env, nxt_java_Request_class,
@@ -625,6 +669,32 @@ nxt_java_Request_isSecure(JNIEnv *env, jclass cls, jlong req_ptr)
static void JNICALL
+nxt_java_Request_upgrade(JNIEnv *env, jclass cls, jlong req_info_ptr)
+{
+ nxt_unit_request_info_t *req;
+
+ req = nxt_jlong2ptr(req_info_ptr);
+
+ if (!nxt_unit_response_is_init(req)) {
+ nxt_unit_response_init(req, 101, 0, 0);
+ }
+
+ (void) nxt_unit_response_upgrade(req);
+}
+
+
+static jboolean JNICALL
+nxt_java_Request_isUpgrade(JNIEnv *env, jclass cls, jlong req_info_ptr)
+{
+ nxt_unit_request_info_t *req;
+
+ req = nxt_jlong2ptr(req_info_ptr);
+
+ return nxt_unit_request_is_websocket_handshake(req);
+}
+
+
+static void JNICALL
nxt_java_Request_log(JNIEnv *env, jclass cls, jlong req_info_ptr, jstring msg,
jint msg_len)
{
@@ -677,3 +747,77 @@ nxt_java_Request_getResponse(JNIEnv *env, jclass cls, jlong req_info_ptr)
return data->jresp;
}
+
+
+static void JNICALL
+nxt_java_Request_sendWsFrameBuf(JNIEnv *env, jclass cls,
+ jlong req_info_ptr, jobject buf, jint pos, jint len, jbyte opCode, jboolean last)
+{
+ nxt_unit_request_info_t *req;
+
+ req = nxt_jlong2ptr(req_info_ptr);
+ uint8_t *b = (*env)->GetDirectBufferAddress(env, buf);
+
+ if (b != NULL) {
+ nxt_unit_websocket_send(req, opCode, last, b + pos, len);
+
+ } else {
+ nxt_unit_req_debug(req, "sendWsFrameBuf: b == NULL");
+ }
+}
+
+
+static void JNICALL
+nxt_java_Request_sendWsFrameArr(JNIEnv *env, jclass cls,
+ jlong req_info_ptr, jarray arr, jint pos, jint len, jbyte opCode, jboolean last)
+{
+ nxt_unit_request_info_t *req;
+
+ req = nxt_jlong2ptr(req_info_ptr);
+ uint8_t *b = (*env)->GetPrimitiveArrayCritical(env, arr, NULL);
+
+ if (b != NULL) {
+ if (!nxt_unit_response_is_sent(req)) {
+ nxt_unit_response_send(req);
+ }
+
+ nxt_unit_websocket_send(req, opCode, last, b + pos, len);
+
+ (*env)->ReleasePrimitiveArrayCritical(env, arr, b, 0);
+
+ } else {
+ nxt_unit_req_debug(req, "sendWsFrameArr: b == NULL");
+ }
+}
+
+
+static void JNICALL
+nxt_java_Request_closeWs(JNIEnv *env, jclass cls, jlong req_info_ptr)
+{
+ nxt_unit_request_info_t *req;
+ nxt_java_request_data_t *data;
+
+ req = nxt_jlong2ptr(req_info_ptr);
+
+ data = req->data;
+
+ (*env)->DeleteGlobalRef(env, data->jresp);
+ (*env)->DeleteGlobalRef(env, data->jreq);
+
+ nxt_unit_request_done(req, NXT_UNIT_OK);
+}
+
+
+void
+nxt_java_Request_websocket(JNIEnv *env, jobject jreq, jobject jbuf,
+ uint8_t opcode, uint8_t fin)
+{
+ (*env)->CallVoidMethod(env, jreq, nxt_java_Request_processWsFrame, jbuf, opcode, fin);
+}
+
+
+void
+nxt_java_Request_close(JNIEnv *env, jobject jreq)
+{
+ (*env)->CallVoidMethod(env, jreq, nxt_java_Request_closeWsSession);
+}
diff --git a/src/java/nxt_jni_Request.h b/src/java/nxt_jni_Request.h
index 1c9c1428..9187d878 100644
--- a/src/java/nxt_jni_Request.h
+++ b/src/java/nxt_jni_Request.h
@@ -15,4 +15,9 @@ int nxt_java_initRequest(JNIEnv *env, jobject cl);
jobject nxt_java_newRequest(JNIEnv *env, jobject ctx, nxt_unit_request_info_t *req);
+void nxt_java_Request_websocket(JNIEnv *env, jobject jreq, jobject jbuf,
+ uint8_t opcode, uint8_t fin);
+
+void nxt_java_Request_close(JNIEnv *env, jobject jreq);
+
#endif /* _NXT_JAVA_REQUEST_H_INCLUDED_ */
diff --git a/src/nxt_application.h b/src/nxt_application.h
index 7ff4bb11..2a1fa39e 100644
--- a/src/nxt_application.h
+++ b/src/nxt_application.h
@@ -88,6 +88,8 @@ struct nxt_common_app_conf_s {
char *working_directory;
nxt_conf_value_t *environment;
+ nxt_conf_value_t *isolation;
+
union {
nxt_external_app_conf_t external;
nxt_python_app_conf_t python;
diff --git a/src/nxt_capability.c b/src/nxt_capability.c
new file mode 100644
index 00000000..805faff6
--- /dev/null
+++ b/src/nxt_capability.c
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) NGINX, Inc.
+ */
+
+#include <nxt_main.h>
+
+#if (NXT_HAVE_LINUX_CAPABILITY)
+
+#include <linux/capability.h>
+#include <sys/syscall.h>
+
+#define nxt_capget(hdrp, datap) \
+ syscall(SYS_capget, hdrp, datap)
+#define nxt_capset(hdrp, datap) \
+ syscall(SYS_capset, hdrp, datap)
+
+#endif /* NXT_HAVE_LINUX_CAPABILITY */
+
+
+static nxt_int_t nxt_capability_specific_set(nxt_task_t *task,
+ nxt_capabilities_t *cap);
+
+
+nxt_int_t
+nxt_capability_set(nxt_task_t *task, nxt_capabilities_t *cap)
+{
+ nxt_assert(cap->setid == 0);
+
+ if (geteuid() == 0) {
+ cap->setid = 1;
+ return NXT_OK;
+ }
+
+ return nxt_capability_specific_set(task, cap);
+}
+
+
+#if (NXT_HAVE_LINUX_CAPABILITY)
+
+static uint32_t
+nxt_capability_linux_get_version()
+{
+ struct __user_cap_header_struct hdr;
+
+ hdr.version = _LINUX_CAPABILITY_VERSION;
+ hdr.pid = nxt_pid;
+
+ nxt_capget(&hdr, NULL);
+ return hdr.version;
+}
+
+
+static nxt_int_t
+nxt_capability_specific_set(nxt_task_t *task, nxt_capabilities_t *cap)
+{
+ struct __user_cap_data_struct *val, data[2];
+ struct __user_cap_header_struct hdr;
+
+ /*
+ * Linux capability v1 fills an u32 struct.
+ * Linux capability v2 and v3 fills an u64 struct.
+ * We allocate data[2] for compatibility, we waste 4 bytes on v1.
+ *
+ * This is safe as we only need to check CAP_SETUID and CAP_SETGID
+ * that resides in the first 32-bit chunk.
+ */
+
+ val = &data[0];
+
+ /*
+ * Ask the kernel the preferred capability version
+ * instead of using _LINUX_CAPABILITY_VERSION from header.
+ * This is safer when distributing a pre-compiled Unit binary.
+ */
+ hdr.version = nxt_capability_linux_get_version();
+ hdr.pid = nxt_pid;
+
+ if (nxt_slow_path(nxt_capget(&hdr, val) == -1)) {
+ nxt_alert(task, "failed to get process capabilities: %E", nxt_errno);
+ return NXT_ERROR;
+ }
+
+ if ((val->effective & (1 << CAP_SETUID)) == 0) {
+ return NXT_OK;
+ }
+
+ if ((val->effective & (1 << CAP_SETGID)) == 0) {
+ return NXT_OK;
+ }
+
+ cap->setid = 1;
+ return NXT_OK;
+}
+
+#else
+
+static nxt_int_t
+nxt_capability_specific_set(nxt_task_t *task, nxt_capabilities_t *cap)
+{
+ return NXT_OK;
+}
+
+#endif
diff --git a/src/nxt_capability.h b/src/nxt_capability.h
new file mode 100644
index 00000000..60bbd5f8
--- /dev/null
+++ b/src/nxt_capability.h
@@ -0,0 +1,17 @@
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) NGINX, Inc.
+ */
+
+#ifndef _NXT_CAPABILITY_INCLUDED_
+#define _NXT_CAPABILITY_INCLUDED_
+
+typedef struct {
+ uint8_t setid; /* 1 bit */
+} nxt_capabilities_t;
+
+
+NXT_EXPORT nxt_int_t nxt_capability_set(nxt_task_t *task,
+ nxt_capabilities_t *cap);
+
+#endif /* _NXT_CAPABILITY_INCLUDED_ */
diff --git a/src/nxt_clone.c b/src/nxt_clone.c
new file mode 100644
index 00000000..0fddd6c7
--- /dev/null
+++ b/src/nxt_clone.c
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) NGINX, Inc.
+ */
+
+#include <nxt_main.h>
+#include <sys/types.h>
+#include <nxt_conf.h>
+#include <nxt_clone.h>
+
+#if (NXT_HAVE_CLONE)
+
+pid_t
+nxt_clone(nxt_int_t flags)
+{
+#if defined(__s390x__) || defined(__s390__) || defined(__CRIS__)
+ return syscall(__NR_clone, NULL, flags);
+#else
+ return syscall(__NR_clone, flags, NULL);
+#endif
+}
+
+#endif
+
+
+#if (NXT_HAVE_CLONE_NEWUSER)
+
+/* map uid 65534 to unit pid */
+#define NXT_DEFAULT_UNPRIV_MAP "65534 %d 1"
+
+nxt_int_t nxt_clone_proc_setgroups(nxt_task_t *task, pid_t child_pid,
+ const char *str);
+nxt_int_t nxt_clone_proc_map_set(nxt_task_t *task, const char* mapfile,
+ pid_t pid, nxt_int_t defval, nxt_conf_value_t *mapobj);
+nxt_int_t nxt_clone_proc_map_write(nxt_task_t *task, const char *mapfile,
+ pid_t pid, u_char *mapinfo);
+
+
+typedef struct {
+ nxt_int_t container;
+ nxt_int_t host;
+ nxt_int_t size;
+} nxt_clone_procmap_t;
+
+
+nxt_int_t
+nxt_clone_proc_setgroups(nxt_task_t *task, pid_t child_pid, const char *str)
+{
+ int fd, n;
+ u_char *p, *end;
+ u_char path[PATH_MAX];
+
+ end = path + PATH_MAX;
+ p = nxt_sprintf(path, end, "/proc/%d/setgroups", child_pid);
+ *p = '\0';
+
+ if (nxt_slow_path(p == end)) {
+ nxt_alert(task, "error write past the buffer: %s", path);
+ return NXT_ERROR;
+ }
+
+ fd = open((char *)path, O_RDWR);
+
+ if (fd == -1) {
+ /*
+ * If the /proc/pid/setgroups doesn't exists, we are
+ * safe to set uid/gid maps. But if the error is anything
+ * other than ENOENT, then we should abort and let user know.
+ */
+
+ if (errno != ENOENT) {
+ nxt_alert(task, "open(%s): %E", path, nxt_errno);
+ return NXT_ERROR;
+ }
+
+ return NXT_OK;
+ }
+
+ n = write(fd, str, strlen(str));
+ close(fd);
+
+ if (nxt_slow_path(n == -1)) {
+ nxt_alert(task, "write(%s): %E", path, nxt_errno);
+ return NXT_ERROR;
+ }
+
+ return NXT_OK;
+}
+
+
+nxt_int_t
+nxt_clone_proc_map_write(nxt_task_t *task, const char *mapfile, pid_t pid,
+ u_char *mapinfo)
+{
+ int len, mapfd;
+ u_char *p, *end;
+ ssize_t n;
+ u_char buf[256];
+
+ end = buf + sizeof(buf);
+
+ p = nxt_sprintf(buf, end, "/proc/%d/%s", pid, mapfile);
+ if (nxt_slow_path(p == end)) {
+ nxt_alert(task, "writing past the buffer");
+ return NXT_ERROR;
+ }
+
+ *p = '\0';
+
+ mapfd = open((char*)buf, O_RDWR);
+ if (nxt_slow_path(mapfd == -1)) {
+ nxt_alert(task, "failed to open proc map (%s) %E", buf, nxt_errno);
+ return NXT_ERROR;
+ }
+
+ len = nxt_strlen(mapinfo);
+
+ n = write(mapfd, (char *)mapinfo, len);
+ if (nxt_slow_path(n != len)) {
+
+ if (n == -1 && nxt_errno == EINVAL) {
+ nxt_alert(task, "failed to write %s: Check kernel maximum " \
+ "allowed lines %E", buf, nxt_errno);
+
+ } else {
+ nxt_alert(task, "failed to write proc map (%s) %E", buf,
+ nxt_errno);
+ }
+
+ return NXT_ERROR;
+ }
+
+ return NXT_OK;
+}
+
+
+nxt_int_t
+nxt_clone_proc_map_set(nxt_task_t *task, const char* mapfile, pid_t pid,
+ nxt_int_t defval, nxt_conf_value_t *mapobj)
+{
+ u_char *p, *end, *mapinfo;
+ nxt_int_t container, host, size;
+ nxt_int_t ret, len, count, i;
+ nxt_conf_value_t *obj, *value;
+
+ static nxt_str_t str_cont = nxt_string("container");
+ static nxt_str_t str_host = nxt_string("host");
+ static nxt_str_t str_size = nxt_string("size");
+
+ /*
+ * uid_map one-entry size:
+ * alloc space for 3 numbers (32bit) plus 2 spaces and \n.
+ */
+ len = sizeof(u_char) * (10 + 10 + 10 + 2 + 1);
+
+ if (mapobj != NULL) {
+ count = nxt_conf_array_elements_count(mapobj);
+
+ if (count == 0) {
+ goto default_map;
+ }
+
+ len = len * count + 1;
+
+ mapinfo = nxt_malloc(len);
+ if (nxt_slow_path(mapinfo == NULL)) {
+ nxt_alert(task, "failed to allocate uid_map buffer");
+ return NXT_ERROR;
+ }
+
+ p = mapinfo;
+ end = mapinfo + len;
+
+ for (i = 0; i < count; i++) {
+ obj = nxt_conf_get_array_element(mapobj, i);
+
+ value = nxt_conf_get_object_member(obj, &str_cont, NULL);
+ container = nxt_conf_get_integer(value);
+
+ value = nxt_conf_get_object_member(obj, &str_host, NULL);
+ host = nxt_conf_get_integer(value);
+
+ value = nxt_conf_get_object_member(obj, &str_size, NULL);
+ size = nxt_conf_get_integer(value);
+
+ p = nxt_sprintf(p, end, "%d %d %d", container, host, size);
+ if (nxt_slow_path(p == end)) {
+ nxt_alert(task, "write past the uid_map buffer");
+ nxt_free(mapinfo);
+ return NXT_ERROR;
+ }
+
+ if (i+1 < count) {
+ *p++ = '\n';
+
+ } else {
+ *p = '\0';
+ }
+ }
+
+ } else {
+
+default_map:
+
+ mapinfo = nxt_malloc(len);
+ if (nxt_slow_path(mapinfo == NULL)) {
+ nxt_alert(task, "failed to allocate uid_map buffer");
+ return NXT_ERROR;
+ }
+
+ end = mapinfo + len;
+ p = nxt_sprintf(mapinfo, end, NXT_DEFAULT_UNPRIV_MAP, defval);
+ *p = '\0';
+
+ if (nxt_slow_path(p == end)) {
+ nxt_alert(task, "write past the %s buffer", mapfile);
+ nxt_free(mapinfo);
+ return NXT_ERROR;
+ }
+ }
+
+ ret = nxt_clone_proc_map_write(task, mapfile, pid, mapinfo);
+
+ nxt_free(mapinfo);
+
+ return ret;
+}
+
+
+nxt_int_t
+nxt_clone_proc_map(nxt_task_t *task, pid_t pid, nxt_process_clone_t *clone)
+{
+ nxt_int_t ret;
+ nxt_int_t uid, gid;
+ const char *rule;
+ nxt_runtime_t *rt;
+
+ rt = task->thread->runtime;
+ uid = geteuid();
+ gid = getegid();
+
+ rule = rt->capabilities.setid ? "allow" : "deny";
+
+ ret = nxt_clone_proc_map_set(task, "uid_map", pid, uid, clone->uidmap);
+ if (nxt_slow_path(ret != NXT_OK)) {
+ return NXT_ERROR;
+ }
+
+ ret = nxt_clone_proc_setgroups(task, pid, rule);
+ if (nxt_slow_path(ret != NXT_OK)) {
+ nxt_alert(task, "failed to write /proc/%d/setgroups", pid);
+ return NXT_ERROR;
+ }
+
+ ret = nxt_clone_proc_map_set(task, "gid_map", pid, gid, clone->gidmap);
+ if (nxt_slow_path(ret != NXT_OK)) {
+ return NXT_ERROR;
+ }
+
+ return NXT_OK;
+}
+
+#endif
diff --git a/src/nxt_clone.h b/src/nxt_clone.h
new file mode 100644
index 00000000..50dec0b4
--- /dev/null
+++ b/src/nxt_clone.h
@@ -0,0 +1,17 @@
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) NGINX, Inc.
+ */
+
+#ifndef _NXT_CLONE_INCLUDED_
+#define _NXT_CLONE_INCLUDED_
+
+
+pid_t nxt_clone(nxt_int_t flags);
+
+#if (NXT_HAVE_CLONE_NEWUSER)
+nxt_int_t nxt_clone_proc_map(nxt_task_t *task, pid_t pid,
+ nxt_process_clone_t *clone);
+#endif
+
+#endif /* _NXT_CLONE_INCLUDED_ */
diff --git a/src/nxt_conf.c b/src/nxt_conf.c
index 57870838..59eddd77 100644
--- a/src/nxt_conf.c
+++ b/src/nxt_conf.c
@@ -16,6 +16,8 @@
#define NXT_CONF_MAX_SHORT_STRING 14
#define NXT_CONF_MAX_STRING NXT_INT32_T_MAX
+#define NXT_CONF_MAX_TOKEN_LEN 256
+
typedef enum {
NXT_CONF_VALUE_NULL = 0,
@@ -90,6 +92,17 @@ struct nxt_conf_op_s {
};
+typedef struct {
+ u_char *start;
+ u_char *end;
+ nxt_bool_t last;
+ u_char buf[NXT_CONF_MAX_TOKEN_LEN];
+} nxt_conf_path_parse_t;
+
+
+static nxt_int_t nxt_conf_path_next_token(nxt_conf_path_parse_t *parse,
+ nxt_str_t *token);
+
static u_char *nxt_conf_json_skip_space(u_char *start, u_char *end);
static u_char *nxt_conf_json_parse_value(nxt_mp_t *mp, nxt_conf_value_t *value,
u_char *start, u_char *end, nxt_conf_json_error_t *error);
@@ -402,22 +415,11 @@ nxt_conf_type(nxt_conf_value_t *value)
}
-typedef struct {
- u_char *start;
- u_char *end;
- nxt_bool_t last;
-} nxt_conf_path_parse_t;
-
-
-static void nxt_conf_path_next_token(nxt_conf_path_parse_t *parse,
- nxt_str_t *token);
-
-
nxt_conf_value_t *
nxt_conf_get_path(nxt_conf_value_t *value, nxt_str_t *path)
{
nxt_str_t token;
- nxt_int_t index;
+ nxt_int_t ret, index;
nxt_conf_path_parse_t parse;
parse.start = path->start;
@@ -425,7 +427,10 @@ nxt_conf_get_path(nxt_conf_value_t *value, nxt_str_t *path)
parse.last = 0;
do {
- nxt_conf_path_next_token(&parse, &token);
+ ret = nxt_conf_path_next_token(&parse, &token);
+ if (nxt_slow_path(ret != NXT_OK)) {
+ return NULL;
+ }
if (token.length == 0) {
@@ -466,24 +471,38 @@ nxt_conf_get_path(nxt_conf_value_t *value, nxt_str_t *path)
}
-static void
+static nxt_int_t
nxt_conf_path_next_token(nxt_conf_path_parse_t *parse, nxt_str_t *token)
{
- u_char *p, *end;
+ u_char *p, *start, *end;
+ size_t length;
- end = parse->end;
- p = parse->start + 1;
+ start = parse->start + 1;
- token->start = p;
+ p = start;
- while (p < end && *p != '/') {
+ while (p < parse->end && *p != '/') {
p++;
}
parse->start = p;
- parse->last = (p >= end);
+ parse->last = (p >= parse->end);
+
+ length = p - start;
+
+ if (nxt_slow_path(length > NXT_CONF_MAX_TOKEN_LEN)) {
+ return NXT_ERROR;
+ }
+
+ end = nxt_decode_uri(parse->buf, start, length);
+ if (nxt_slow_path(end == NULL)) {
+ return NXT_ERROR;
+ }
- token->length = p - token->start;
+ token->length = end - parse->buf;
+ token->start = parse->buf;
+
+ return NXT_OK;
}
@@ -742,7 +761,7 @@ nxt_conf_op_compile(nxt_mp_t *mp, nxt_conf_op_t **ops, nxt_conf_value_t *root,
nxt_str_t *path, nxt_conf_value_t *value, nxt_bool_t add)
{
nxt_str_t token;
- nxt_int_t index;
+ nxt_int_t ret, index;
nxt_conf_op_t *op, **parent;
nxt_conf_value_t *node;
nxt_conf_path_parse_t parse;
@@ -763,7 +782,10 @@ nxt_conf_op_compile(nxt_mp_t *mp, nxt_conf_op_t **ops, nxt_conf_value_t *root,
*parent = op;
parent = (nxt_conf_op_t **) &op->ctx;
- nxt_conf_path_next_token(&parse, &token);
+ ret = nxt_conf_path_next_token(&parse, &token);
+ if (nxt_slow_path(ret != NXT_OK)) {
+ return NXT_CONF_OP_ERROR;
+ }
switch (root->type) {
@@ -857,7 +879,10 @@ nxt_conf_op_compile(nxt_mp_t *mp, nxt_conf_op_t **ops, nxt_conf_value_t *root,
return NXT_CONF_OP_ERROR;
}
- nxt_conf_set_string(&member->name, &token);
+ ret = nxt_conf_set_string_dup(&member->name, mp, &token);
+ if (nxt_slow_path(ret != NXT_OK)) {
+ return NXT_CONF_OP_ERROR;
+ }
member->value = *value;
diff --git a/src/nxt_conf.h b/src/nxt_conf.h
index 2435b0e2..725a6c95 100644
--- a/src/nxt_conf.h
+++ b/src/nxt_conf.h
@@ -71,6 +71,7 @@ typedef struct {
nxt_conf_value_t *conf;
nxt_mp_t *pool;
nxt_str_t error;
+ void *ctx;
} nxt_conf_validation_t;
diff --git a/src/nxt_conf_validation.c b/src/nxt_conf_validation.c
index ca8ec62e..c934b10b 100644
--- a/src/nxt_conf_validation.c
+++ b/src/nxt_conf_validation.c
@@ -8,6 +8,7 @@
#include <nxt_conf.h>
#include <nxt_cert.h>
#include <nxt_router.h>
+#include <nxt_http.h>
typedef enum {
@@ -39,15 +40,18 @@ typedef nxt_int_t (*nxt_conf_vldt_member_t)(nxt_conf_validation_t *vldt,
nxt_conf_value_t *value);
typedef nxt_int_t (*nxt_conf_vldt_element_t)(nxt_conf_validation_t *vldt,
nxt_conf_value_t *value);
-typedef nxt_int_t (*nxt_conf_vldt_system_t)(nxt_conf_validation_t *vldt,
- char *name);
-
static nxt_int_t nxt_conf_vldt_type(nxt_conf_validation_t *vldt,
nxt_str_t *name, nxt_conf_value_t *value, nxt_conf_vldt_type_t type);
static nxt_int_t nxt_conf_vldt_error(nxt_conf_validation_t *vldt,
const char *fmt, ...);
+static nxt_int_t nxt_conf_vldt_mtypes(nxt_conf_validation_t *vldt,
+ nxt_conf_value_t *value, void *data);
+static nxt_int_t nxt_conf_vldt_mtypes_type(nxt_conf_validation_t *vldt,
+ nxt_str_t *name, nxt_conf_value_t *value);
+static nxt_int_t nxt_conf_vldt_mtypes_extension(nxt_conf_validation_t *vldt,
+ nxt_conf_value_t *value);
static nxt_int_t nxt_conf_vldt_listener(nxt_conf_validation_t *vldt,
nxt_str_t *name, nxt_conf_value_t *value);
#if (NXT_TLS)
@@ -86,10 +90,6 @@ static nxt_int_t nxt_conf_vldt_object_iterator(nxt_conf_validation_t *vldt,
nxt_conf_value_t *value, void *data);
static nxt_int_t nxt_conf_vldt_array_iterator(nxt_conf_validation_t *vldt,
nxt_conf_value_t *value, void *data);
-static nxt_int_t nxt_conf_vldt_system(nxt_conf_validation_t *vldt,
- nxt_conf_value_t *value, void *data);
-static nxt_int_t nxt_conf_vldt_user(nxt_conf_validation_t *vldt, char *name);
-static nxt_int_t nxt_conf_vldt_group(nxt_conf_validation_t *vldt, char *name);
static nxt_int_t nxt_conf_vldt_environment(nxt_conf_validation_t *vldt,
nxt_str_t *name, nxt_conf_value_t *value);
static nxt_int_t nxt_conf_vldt_argument(nxt_conf_validation_t *vldt,
@@ -101,6 +101,21 @@ static nxt_int_t nxt_conf_vldt_java_classpath(nxt_conf_validation_t *vldt,
static nxt_int_t nxt_conf_vldt_java_option(nxt_conf_validation_t *vldt,
nxt_conf_value_t *value);
+static nxt_int_t
+nxt_conf_vldt_isolation(nxt_conf_validation_t *vldt, nxt_conf_value_t *value,
+ void *data);
+static nxt_int_t
+nxt_conf_vldt_clone_namespaces(nxt_conf_validation_t *vldt,
+ nxt_conf_value_t *value, void *data);
+
+#if (NXT_HAVE_CLONE_NEWUSER)
+static nxt_int_t nxt_conf_vldt_clone_procmap(nxt_conf_validation_t *vldt,
+ const char* mapfile, nxt_conf_value_t *value);
+static nxt_int_t nxt_conf_vldt_clone_uidmap(nxt_conf_validation_t *vldt,
+ nxt_conf_value_t *value);
+static nxt_int_t nxt_conf_vldt_clone_gidmap(nxt_conf_validation_t *vldt,
+ nxt_conf_value_t *value);
+#endif
static nxt_conf_vldt_object_t nxt_conf_vldt_websocket_members[] = {
{ nxt_string("read_timeout"),
@@ -122,6 +137,16 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_websocket_members[] = {
};
+static nxt_conf_vldt_object_t nxt_conf_vldt_static_members[] = {
+ { nxt_string("mime_types"),
+ NXT_CONF_VLDT_OBJECT,
+ &nxt_conf_vldt_mtypes,
+ NULL },
+
+ NXT_CONF_VLDT_END
+};
+
+
static nxt_conf_vldt_object_t nxt_conf_vldt_http_members[] = {
{ nxt_string("header_read_timeout"),
NXT_CONF_VLDT_INTEGER,
@@ -153,6 +178,11 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_http_members[] = {
&nxt_conf_vldt_object,
(void *) &nxt_conf_vldt_websocket_members },
+ { nxt_string("static"),
+ NXT_CONF_VLDT_OBJECT,
+ &nxt_conf_vldt_object,
+ (void *) &nxt_conf_vldt_static_members },
+
NXT_CONF_VLDT_END
};
@@ -281,6 +311,11 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_action_members[] = {
&nxt_conf_vldt_pass,
NULL },
+ { nxt_string("share"),
+ NXT_CONF_VLDT_STRING,
+ NULL,
+ NULL },
+
NXT_CONF_VLDT_END
};
@@ -340,6 +375,100 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_app_processes_members[] = {
};
+static nxt_conf_vldt_object_t nxt_conf_vldt_app_namespaces_members[] = {
+
+#if (NXT_HAVE_CLONE_NEWUSER)
+ { nxt_string("credential"),
+ NXT_CONF_VLDT_BOOLEAN,
+ NULL,
+ NULL },
+#endif
+
+#if (NXT_HAVE_CLONE_NEWPID)
+ { nxt_string("pid"),
+ NXT_CONF_VLDT_BOOLEAN,
+ NULL,
+ NULL },
+#endif
+
+#if (NXT_HAVE_CLONE_NEWNET)
+ { nxt_string("network"),
+ NXT_CONF_VLDT_BOOLEAN,
+ NULL,
+ NULL },
+#endif
+
+#if (NXT_HAVE_CLONE_NEWNS)
+ { nxt_string("mount"),
+ NXT_CONF_VLDT_BOOLEAN,
+ NULL,
+ NULL },
+#endif
+
+#if (NXT_HAVE_CLONE_NEWUTS)
+ { nxt_string("uname"),
+ NXT_CONF_VLDT_BOOLEAN,
+ NULL,
+ NULL },
+#endif
+
+#if (NXT_HAVE_CLONE_NEWCGROUP)
+ { nxt_string("cgroup"),
+ NXT_CONF_VLDT_BOOLEAN,
+ NULL,
+ NULL },
+#endif
+
+ NXT_CONF_VLDT_END
+};
+
+
+#if (NXT_HAVE_CLONE_NEWUSER)
+
+static nxt_conf_vldt_object_t nxt_conf_vldt_app_procmap_members[] = {
+ { nxt_string("container"),
+ NXT_CONF_VLDT_INTEGER,
+ NULL,
+ NULL },
+
+ { nxt_string("host"),
+ NXT_CONF_VLDT_INTEGER,
+ NULL,
+ NULL },
+
+ { nxt_string("size"),
+ NXT_CONF_VLDT_INTEGER,
+ NULL,
+ NULL },
+};
+
+#endif
+
+
+static nxt_conf_vldt_object_t nxt_conf_vldt_app_isolation_members[] = {
+ { nxt_string("namespaces"),
+ NXT_CONF_VLDT_OBJECT,
+ &nxt_conf_vldt_clone_namespaces,
+ (void *) &nxt_conf_vldt_app_namespaces_members },
+
+#if (NXT_HAVE_CLONE_NEWUSER)
+
+ { nxt_string("uidmap"),
+ NXT_CONF_VLDT_ARRAY,
+ &nxt_conf_vldt_array_iterator,
+ (void *) &nxt_conf_vldt_clone_uidmap },
+
+ { nxt_string("gidmap"),
+ NXT_CONF_VLDT_ARRAY,
+ &nxt_conf_vldt_array_iterator,
+ (void *) &nxt_conf_vldt_clone_gidmap },
+
+#endif
+
+ NXT_CONF_VLDT_END
+};
+
+
static nxt_conf_vldt_object_t nxt_conf_vldt_common_members[] = {
{ nxt_string("type"),
NXT_CONF_VLDT_STRING,
@@ -358,13 +487,13 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_common_members[] = {
{ nxt_string("user"),
NXT_CONF_VLDT_STRING,
- nxt_conf_vldt_system,
- (void *) &nxt_conf_vldt_user },
+ NULL,
+ NULL },
{ nxt_string("group"),
NXT_CONF_VLDT_STRING,
- nxt_conf_vldt_system,
- (void *) &nxt_conf_vldt_group },
+ NULL,
+ NULL },
{ nxt_string("working_directory"),
NXT_CONF_VLDT_STRING,
@@ -376,6 +505,11 @@ static nxt_conf_vldt_object_t nxt_conf_vldt_common_members[] = {
&nxt_conf_vldt_object_iterator,
(void *) &nxt_conf_vldt_environment },
+ { nxt_string("isolation"),
+ NXT_CONF_VLDT_OBJECT,
+ &nxt_conf_vldt_isolation,
+ (void *) &nxt_conf_vldt_app_isolation_members },
+
NXT_CONF_VLDT_END
};
@@ -628,6 +762,108 @@ nxt_conf_vldt_error(nxt_conf_validation_t *vldt, const char *fmt, ...)
}
+typedef struct {
+ nxt_mp_t *pool;
+ nxt_str_t *type;
+ nxt_lvlhsh_t hash;
+} nxt_conf_vldt_mtypes_ctx_t;
+
+
+static nxt_int_t
+nxt_conf_vldt_mtypes(nxt_conf_validation_t *vldt, nxt_conf_value_t *value,
+ void *data)
+{
+ nxt_int_t ret;
+ nxt_conf_vldt_mtypes_ctx_t ctx;
+
+ ctx.pool = nxt_mp_create(1024, 128, 256, 32);
+ if (nxt_slow_path(ctx.pool == NULL)) {
+ return NXT_ERROR;
+ }
+
+ nxt_lvlhsh_init(&ctx.hash);
+
+ vldt->ctx = &ctx;
+
+ ret = nxt_conf_vldt_object_iterator(vldt, value,
+ &nxt_conf_vldt_mtypes_type);
+
+ vldt->ctx = NULL;
+
+ nxt_mp_destroy(ctx.pool);
+
+ return ret;
+}
+
+
+static nxt_int_t
+nxt_conf_vldt_mtypes_type(nxt_conf_validation_t *vldt, nxt_str_t *name,
+ nxt_conf_value_t *value)
+{
+ nxt_int_t ret;
+ nxt_conf_vldt_mtypes_ctx_t *ctx;
+
+ ret = nxt_conf_vldt_type(vldt, name, value,
+ NXT_CONF_VLDT_STRING|NXT_CONF_VLDT_ARRAY);
+ if (ret != NXT_OK) {
+ return ret;
+ }
+
+ ctx = vldt->ctx;
+
+ ctx->type = nxt_mp_get(ctx->pool, sizeof(nxt_str_t));
+ if (nxt_slow_path(ctx->type == NULL)) {
+ return NXT_ERROR;
+ }
+
+ *ctx->type = *name;
+
+ if (nxt_conf_type(value) == NXT_CONF_ARRAY) {
+ return nxt_conf_vldt_array_iterator(vldt, value,
+ &nxt_conf_vldt_mtypes_extension);
+ }
+
+ /* NXT_CONF_STRING */
+
+ return nxt_conf_vldt_mtypes_extension(vldt, value);
+}
+
+
+static nxt_int_t
+nxt_conf_vldt_mtypes_extension(nxt_conf_validation_t *vldt,
+ nxt_conf_value_t *value)
+{
+ nxt_str_t ext, *dup_type;
+ nxt_conf_vldt_mtypes_ctx_t *ctx;
+
+ ctx = vldt->ctx;
+
+ if (nxt_conf_type(value) != NXT_CONF_STRING) {
+ return nxt_conf_vldt_error(vldt, "The \"%V\" MIME type array must "
+ "contain only strings.", ctx->type);
+ }
+
+ nxt_conf_get_string(value, &ext);
+
+ if (ext.length == 0) {
+ return nxt_conf_vldt_error(vldt, "An empty file extension for "
+ "the \"%V\" MIME type.", ctx->type);
+ }
+
+ dup_type = nxt_http_static_mtypes_hash_find(&ctx->hash, &ext);
+
+ if (dup_type != NULL) {
+ return nxt_conf_vldt_error(vldt, "The \"%V\" file extension has been "
+ "declared for \"%V\" and \"%V\" "
+ "MIME types at the same time.",
+ &ext, dup_type, ctx->type);
+ }
+
+ return nxt_http_static_mtypes_hash_add(ctx->pool, &ctx->hash,
+ &ext, ctx->type);
+}
+
+
static nxt_int_t
nxt_conf_vldt_listener(nxt_conf_validation_t *vldt, nxt_str_t *name,
nxt_conf_value_t *value)
@@ -1252,106 +1488,168 @@ nxt_conf_vldt_array_iterator(nxt_conf_validation_t *vldt,
static nxt_int_t
-nxt_conf_vldt_system(nxt_conf_validation_t *vldt, nxt_conf_value_t *value,
- void *data)
+nxt_conf_vldt_environment(nxt_conf_validation_t *vldt, nxt_str_t *name,
+ nxt_conf_value_t *value)
{
- size_t length;
- nxt_str_t name;
- nxt_conf_vldt_system_t validator;
- char string[32];
+ nxt_str_t str;
- /* The cast is required by Sun C. */
- validator = (nxt_conf_vldt_system_t) data;
+ if (name->length == 0) {
+ return nxt_conf_vldt_error(vldt,
+ "The environment name must not be empty.");
+ }
- nxt_conf_get_string(value, &name);
+ if (nxt_memchr(name->start, '\0', name->length) != NULL) {
+ return nxt_conf_vldt_error(vldt, "The environment name must not "
+ "contain null character.");
+ }
- length = name.length + 1;
- length = nxt_min(length, sizeof(string));
+ if (nxt_memchr(name->start, '=', name->length) != NULL) {
+ return nxt_conf_vldt_error(vldt, "The environment name must not "
+ "contain '=' character.");
+ }
- nxt_cpystrn((u_char *) string, name.start, length);
+ if (nxt_conf_type(value) != NXT_CONF_STRING) {
+ return nxt_conf_vldt_error(vldt, "The \"%V\" environment value must be "
+ "a string.", name);
+ }
+
+ nxt_conf_get_string(value, &str);
- return validator(vldt, string);
+ if (nxt_memchr(str.start, '\0', str.length) != NULL) {
+ return nxt_conf_vldt_error(vldt, "The \"%V\" environment value must "
+ "not contain null character.", name);
+ }
+
+ return NXT_OK;
}
static nxt_int_t
-nxt_conf_vldt_user(nxt_conf_validation_t *vldt, char *user)
+nxt_conf_vldt_clone_namespaces(nxt_conf_validation_t *vldt, nxt_conf_value_t *value,
+ void *data)
{
- struct passwd *pwd;
+ return nxt_conf_vldt_object(vldt, value, data);
+}
- nxt_errno = 0;
- pwd = getpwnam(user);
+static nxt_int_t
+nxt_conf_vldt_isolation(nxt_conf_validation_t *vldt, nxt_conf_value_t *value,
+ void *data)
+{
+ return nxt_conf_vldt_object(vldt, value, data);
+}
- if (pwd != NULL) {
- return NXT_OK;
- }
- if (nxt_errno == 0) {
- return nxt_conf_vldt_error(vldt, "User \"%s\" is not found.", user);
- }
+#if (NXT_HAVE_CLONE_NEWUSER)
+
+typedef struct {
+ nxt_int_t container;
+ nxt_int_t host;
+ nxt_int_t size;
+} nxt_conf_vldt_clone_procmap_conf_t;
- return NXT_ERROR;
-}
+
+static nxt_conf_map_t nxt_conf_vldt_clone_procmap_conf_map[] = {
+ {
+ nxt_string("container"),
+ NXT_CONF_MAP_INT32,
+ offsetof(nxt_conf_vldt_clone_procmap_conf_t, container),
+ },
+
+ {
+ nxt_string("host"),
+ NXT_CONF_MAP_INT32,
+ offsetof(nxt_conf_vldt_clone_procmap_conf_t, host),
+ },
+
+ {
+ nxt_string("size"),
+ NXT_CONF_MAP_INT32,
+ offsetof(nxt_conf_vldt_clone_procmap_conf_t, size),
+ },
+
+};
static nxt_int_t
-nxt_conf_vldt_group(nxt_conf_validation_t *vldt, char *group)
+nxt_conf_vldt_clone_procmap(nxt_conf_validation_t *vldt, const char *mapfile,
+ nxt_conf_value_t *value)
{
- struct group *grp;
+ nxt_int_t ret;
+ nxt_conf_vldt_clone_procmap_conf_t procmap;
- nxt_errno = 0;
+ procmap.container = -1;
+ procmap.host = -1;
+ procmap.size = -1;
- grp = getgrnam(group);
+ ret = nxt_conf_map_object(vldt->pool, value,
+ nxt_conf_vldt_clone_procmap_conf_map,
+ nxt_nitems(nxt_conf_vldt_clone_procmap_conf_map),
+ &procmap);
+ if (ret != NXT_OK) {
+ return ret;
+ }
- if (grp != NULL) {
- return NXT_OK;
+ if (procmap.container == -1) {
+ return nxt_conf_vldt_error(vldt, "The %s requires the "
+ "\"container\" field set.", mapfile);
}
- if (nxt_errno == 0) {
- return nxt_conf_vldt_error(vldt, "Group \"%s\" is not found.", group);
+ if (procmap.host == -1) {
+ return nxt_conf_vldt_error(vldt, "The %s requires the "
+ "\"host\" field set.", mapfile);
}
- return NXT_ERROR;
+ if (procmap.size == -1) {
+ return nxt_conf_vldt_error(vldt, "The %s requires the "
+ "\"size\" field set.", mapfile);
+ }
+
+ return NXT_OK;
}
static nxt_int_t
-nxt_conf_vldt_environment(nxt_conf_validation_t *vldt, nxt_str_t *name,
- nxt_conf_value_t *value)
+nxt_conf_vldt_clone_uidmap(nxt_conf_validation_t *vldt, nxt_conf_value_t *value)
{
- nxt_str_t str;
+ nxt_int_t ret;
- if (name->length == 0) {
- return nxt_conf_vldt_error(vldt,
- "The environment name must not be empty.");
+ if (nxt_conf_type(value) != NXT_CONF_OBJECT) {
+ return nxt_conf_vldt_error(vldt, "The \"uidmap\" array "
+ "must contain only object values.");
}
- if (nxt_memchr(name->start, '\0', name->length) != NULL) {
- return nxt_conf_vldt_error(vldt, "The environment name must not "
- "contain null character.");
+ ret = nxt_conf_vldt_object(vldt, value,
+ (void *) nxt_conf_vldt_app_procmap_members);
+ if (nxt_slow_path(ret != NXT_OK)) {
+ return ret;
}
- if (nxt_memchr(name->start, '=', name->length) != NULL) {
- return nxt_conf_vldt_error(vldt, "The environment name must not "
- "contain '=' character.");
- }
+ return nxt_conf_vldt_clone_procmap(vldt, "uid_map", value);
+}
- if (nxt_conf_type(value) != NXT_CONF_STRING) {
- return nxt_conf_vldt_error(vldt, "The \"%V\" environment value must be "
- "a string.", name);
- }
- nxt_conf_get_string(value, &str);
+static nxt_int_t
+nxt_conf_vldt_clone_gidmap(nxt_conf_validation_t *vldt, nxt_conf_value_t *value)
+{
+ nxt_int_t ret;
- if (nxt_memchr(str.start, '\0', str.length) != NULL) {
- return nxt_conf_vldt_error(vldt, "The \"%V\" environment value must "
- "not contain null character.", name);
+ if (nxt_conf_type(value) != NXT_CONF_OBJECT) {
+ return nxt_conf_vldt_error(vldt, "The \"gidmap\" array "
+ "must contain only object values.");
}
- return NXT_OK;
+ ret = nxt_conf_vldt_object(vldt, value,
+ (void *) nxt_conf_vldt_app_procmap_members);
+ if (nxt_slow_path(ret != NXT_OK)) {
+ return ret;
+ }
+
+ return nxt_conf_vldt_clone_procmap(vldt, "gid_map", value);
}
+#endif
+
static nxt_int_t
nxt_conf_vldt_argument(nxt_conf_validation_t *vldt, nxt_conf_value_t *value)
diff --git a/src/nxt_controller.c b/src/nxt_controller.c
index 49afbe46..86ba1246 100644
--- a/src/nxt_controller.c
+++ b/src/nxt_controller.c
@@ -478,6 +478,8 @@ nxt_controller_conn_init(nxt_task_t *task, void *obj, void *data)
return;
}
+ r->parser.encoded_slashes = 1;
+
b = nxt_buf_mem_alloc(c->mem_pool, 1024, 0);
if (nxt_slow_path(b == NULL)) {
nxt_controller_conn_free(task, c, NULL);
diff --git a/src/nxt_h1proto.c b/src/nxt_h1proto.c
index a60bdb36..39be4315 100644
--- a/src/nxt_h1proto.c
+++ b/src/nxt_h1proto.c
@@ -45,7 +45,7 @@ static void nxt_h1p_conn_request_body_read(nxt_task_t *task, void *obj,
void *data);
static void nxt_h1p_request_local_addr(nxt_task_t *task, nxt_http_request_t *r);
static void nxt_h1p_request_header_send(nxt_task_t *task,
- nxt_http_request_t *r);
+ nxt_http_request_t *r, nxt_work_handler_t body_handler);
static void nxt_h1p_request_send(nxt_task_t *task, nxt_http_request_t *r,
nxt_buf_t *out);
static nxt_buf_t *nxt_h1p_chunk_create(nxt_task_t *task, nxt_http_request_t *r,
@@ -830,8 +830,7 @@ nxt_h1p_request_body_read(nxt_task_t *task, nxt_http_request_t *r)
ready:
- nxt_work_queue_add(&task->thread->engine->fast_work_queue,
- r->state->ready_handler, task, r, NULL);
+ r->state->ready_handler(task, r, NULL);
return;
@@ -884,8 +883,7 @@ nxt_h1p_conn_request_body_read(nxt_task_t *task, void *obj, void *data)
c->read = NULL;
r = h1p->request;
- nxt_work_queue_add(&engine->fast_work_queue, r->state->ready_handler,
- task, r, NULL);
+ r->state->ready_handler(task, r, NULL);
}
}
@@ -996,7 +994,8 @@ static const nxt_str_t nxt_http_server_error[] = {
#define UNKNOWN_STATUS_LENGTH nxt_length("HTTP/1.1 65536\r\n")
static void
-nxt_h1p_request_header_send(nxt_task_t *task, nxt_http_request_t *r)
+nxt_h1p_request_header_send(nxt_task_t *task, nxt_http_request_t *r,
+ nxt_work_handler_t body_handler)
{
u_char *p;
size_t size;
@@ -1079,6 +1078,7 @@ nxt_h1p_request_header_send(nxt_task_t *task, nxt_http_request_t *r)
if (http11) {
if (n != NXT_HTTP_NOT_MODIFIED
&& n != NXT_HTTP_NO_CONTENT
+ && body_handler != NULL
&& !h1p->websocket)
{
h1p->chunked = 1;
@@ -1165,6 +1165,19 @@ nxt_h1p_request_header_send(nxt_task_t *task, nxt_http_request_t *r)
c->write = header;
c->write_state = &nxt_h1p_request_send_state;
+ if (body_handler != NULL) {
+ /*
+ * The body handler will run before c->io->write() handler,
+ * because the latter was inqueued by nxt_conn_write()
+ * in engine->write_work_queue.
+ */
+ nxt_work_queue_add(&task->thread->engine->fast_work_queue,
+ body_handler, task, r, NULL);
+
+ } else {
+ header->next = nxt_http_buf_last(r);
+ }
+
nxt_conn_write(task->thread->engine, c);
if (h1p->websocket) {
diff --git a/src/nxt_h1proto_websocket.c b/src/nxt_h1proto_websocket.c
index dd9b6848..13754be0 100644
--- a/src/nxt_h1proto_websocket.c
+++ b/src/nxt_h1proto_websocket.c
@@ -417,16 +417,13 @@ nxt_h1p_conn_ws_frame_process(nxt_task_t *task, nxt_conn_t *c,
uint8_t *p, *mask;
uint16_t code;
nxt_http_request_t *r;
- nxt_event_engine_t *engine;
- engine = task->thread->engine;
r = h1p->request;
c->read = NULL;
if (nxt_slow_path(wsh->opcode == NXT_WEBSOCKET_OP_PING)) {
- nxt_work_queue_add(&engine->fast_work_queue, nxt_h1p_conn_ws_pong,
- task, r, NULL);
+ nxt_h1p_conn_ws_pong(task, r, NULL);
return;
}
@@ -451,8 +448,7 @@ nxt_h1p_conn_ws_frame_process(nxt_task_t *task, nxt_conn_t *c,
h1p->websocket_closed = 1;
}
- nxt_work_queue_add(&engine->fast_work_queue, r->state->ready_handler,
- task, r, NULL);
+ r->state->ready_handler(task, r, NULL);
}
diff --git a/src/nxt_http.h b/src/nxt_http.h
index ac1eedcf..560b7310 100644
--- a/src/nxt_http.h
+++ b/src/nxt_http.h
@@ -24,7 +24,9 @@ typedef enum {
NXT_HTTP_NOT_MODIFIED = 304,
NXT_HTTP_BAD_REQUEST = 400,
+ NXT_HTTP_FORBIDDEN = 403,
NXT_HTTP_NOT_FOUND = 404,
+ NXT_HTTP_METHOD_NOT_ALLOWED = 405,
NXT_HTTP_REQUEST_TIMEOUT = 408,
NXT_HTTP_LENGTH_REQUIRED = 411,
NXT_HTTP_PAYLOAD_TOO_LARGE = 413,
@@ -175,7 +177,8 @@ struct nxt_http_pass_s {
typedef struct {
void (*body_read)(nxt_task_t *task, nxt_http_request_t *r);
void (*local_addr)(nxt_task_t *task, nxt_http_request_t *r);
- void (*header_send)(nxt_task_t *task, nxt_http_request_t *r);
+ void (*header_send)(nxt_task_t *task, nxt_http_request_t *r,
+ nxt_work_handler_t body_handler);
void (*send)(nxt_task_t *task, nxt_http_request_t *r, nxt_buf_t *out);
nxt_off_t (*body_bytes_sent)(nxt_task_t *task, nxt_http_proto_t proto);
void (*discard)(nxt_task_t *task, nxt_http_request_t *r, nxt_buf_t *last);
@@ -186,6 +189,25 @@ typedef struct {
} nxt_http_proto_table_t;
+#define NXT_HTTP_DATE_LEN nxt_length("Wed, 31 Dec 1986 16:40:00 GMT")
+
+nxt_inline u_char *
+nxt_http_date(u_char *buf, struct tm *tm)
+{
+ static const char *week[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri",
+ "Sat" };
+
+ static const char *month[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
+
+ return nxt_sprintf(buf, buf + NXT_HTTP_DATE_LEN,
+ "%s, %02d %s %4d %02d:%02d:%02d GMT",
+ week[tm->tm_wday], tm->tm_mday,
+ month[tm->tm_mon], tm->tm_year + 1900,
+ tm->tm_hour, tm->tm_min, tm->tm_sec);
+}
+
+
nxt_int_t nxt_http_init(nxt_task_t *task, nxt_runtime_t *rt);
nxt_int_t nxt_h1p_init(nxt_task_t *task, nxt_runtime_t *rt);
nxt_int_t nxt_http_response_hash_init(nxt_task_t *task, nxt_runtime_t *rt);
@@ -195,7 +217,8 @@ nxt_http_request_t *nxt_http_request_create(nxt_task_t *task);
void nxt_http_request_error(nxt_task_t *task, nxt_http_request_t *r,
nxt_http_status_t status);
void nxt_http_request_read_body(nxt_task_t *task, nxt_http_request_t *r);
-void nxt_http_request_header_send(nxt_task_t *task, nxt_http_request_t *r);
+void nxt_http_request_header_send(nxt_task_t *task, nxt_http_request_t *r,
+ nxt_work_handler_t body_handler);
void nxt_http_request_ws_frame_start(nxt_task_t *task, nxt_http_request_t *r,
nxt_buf_t *ws_frame);
void nxt_http_request_send(nxt_task_t *task, nxt_http_request_t *r,
@@ -223,6 +246,14 @@ nxt_http_pass_t *nxt_http_pass_application(nxt_task_t *task,
void nxt_http_routes_cleanup(nxt_task_t *task, nxt_http_routes_t *routes);
void nxt_http_pass_cleanup(nxt_task_t *task, nxt_http_pass_t *pass);
+nxt_http_pass_t *nxt_http_static_handler(nxt_task_t *task,
+ nxt_http_request_t *r, nxt_http_pass_t *pass);
+nxt_int_t nxt_http_static_mtypes_init(nxt_mp_t *mp, nxt_lvlhsh_t *hash);
+nxt_int_t nxt_http_static_mtypes_hash_add(nxt_mp_t *mp, nxt_lvlhsh_t *hash,
+ nxt_str_t *extension, nxt_str_t *type);
+nxt_str_t *nxt_http_static_mtypes_hash_find(nxt_lvlhsh_t *hash,
+ nxt_str_t *extension);
+
nxt_http_pass_t *nxt_http_request_application(nxt_task_t *task,
nxt_http_request_t *r, nxt_http_pass_t *pass);
diff --git a/src/nxt_http_error.c b/src/nxt_http_error.c
index c7c7e81a..1dcd8783 100644
--- a/src/nxt_http_error.c
+++ b/src/nxt_http_error.c
@@ -51,12 +51,10 @@ nxt_http_request_error(nxt_task_t *task, nxt_http_request_t *r,
r->resp.content_length = NULL;
r->resp.content_length_n = nxt_length(error);
- nxt_http_request_header_send(task, r);
-
r->state = &nxt_http_request_send_error_body_state;
- nxt_work_queue_add(&task->thread->engine->fast_work_queue,
- nxt_http_request_send_error_body, task, r, NULL);
+ nxt_http_request_header_send(task, r, nxt_http_request_send_error_body);
+
return;
fail:
diff --git a/src/nxt_http_parse.c b/src/nxt_http_parse.c
index 8b4bf47c..5b009d96 100644
--- a/src/nxt_http_parse.c
+++ b/src/nxt_http_parse.c
@@ -47,7 +47,6 @@ typedef enum {
NXT_HTTP_TARGET_DOT, /* . */
NXT_HTTP_TARGET_ARGS_MARK, /* ? */
NXT_HTTP_TARGET_QUOTE_MARK, /* % */
- NXT_HTTP_TARGET_PLUS, /* + */
} nxt_http_target_traps_e;
@@ -57,7 +56,7 @@ static const uint8_t nxt_http_target_chars[256] nxt_aligned(64) = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* \s ! " # $ % & ' ( ) * + , - . / */
- 1, 0, 0, 2, 0, 8, 0, 0, 0, 0, 0, 9, 0, 0, 6, 5,
+ 1, 0, 0, 2, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 6, 5,
/* 0 1 2 3 4 5 6 7 8 9 : ; < = > ? */
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7,
@@ -163,8 +162,9 @@ static nxt_int_t
nxt_http_parse_request_line(nxt_http_request_parse_t *rp, u_char **pos,
u_char *end)
{
- u_char *p, ch, *after_slash;
+ u_char *p, ch, *after_slash, *exten, *args;
nxt_int_t rc;
+ nxt_bool_t rest;
nxt_http_ver_t ver;
nxt_http_target_traps_e trap;
@@ -255,6 +255,11 @@ nxt_http_parse_request_line(nxt_http_request_parse_t *rp, u_char **pos,
rp->target_start = p;
after_slash = p + 1;
+ exten = NULL;
+ args = NULL;
+ rest = 0;
+
+continue_target:
for ( ;; ) {
p++;
@@ -269,8 +274,7 @@ nxt_http_parse_request_line(nxt_http_request_parse_t *rp, u_char **pos,
}
after_slash = p + 1;
-
- rp->exten_start = NULL;
+ exten = NULL;
continue;
case NXT_HTTP_TARGET_DOT:
@@ -279,11 +283,11 @@ nxt_http_parse_request_line(nxt_http_request_parse_t *rp, u_char **pos,
goto rest_of_target;
}
- rp->exten_start = p + 1;
+ exten = p + 1;
continue;
case NXT_HTTP_TARGET_ARGS_MARK:
- rp->args_start = p + 1;
+ args = p + 1;
goto rest_of_target;
case NXT_HTTP_TARGET_SPACE:
@@ -294,10 +298,6 @@ nxt_http_parse_request_line(nxt_http_request_parse_t *rp, u_char **pos,
rp->quoted_target = 1;
goto rest_of_target;
- case NXT_HTTP_TARGET_PLUS:
- rp->plus_in_target = 1;
- continue;
-
case NXT_HTTP_TARGET_HASH:
rp->complex_target = 1;
goto rest_of_target;
@@ -316,6 +316,8 @@ nxt_http_parse_request_line(nxt_http_request_parse_t *rp, u_char **pos,
rest_of_target:
+ rest = 1;
+
for ( ;; ) {
p++;
@@ -382,7 +384,12 @@ space_after_target:
}
rp->space_in_target = 1;
- goto rest_of_target;
+
+ if (rest) {
+ goto rest_of_target;
+ }
+
+ goto continue_target;
}
/* " HTTP/1.1\r\n" or " HTTP/1.1\n" */
@@ -396,7 +403,12 @@ space_after_target:
}
rp->space_in_target = 1;
- goto rest_of_target;
+
+ if (rest) {
+ goto rest_of_target;
+ }
+
+ goto continue_target;
}
nxt_memcpy(ver.str, &p[1], 8);
@@ -437,20 +449,19 @@ space_after_target:
rp->path.start = rp->target_start;
- if (rp->args_start != NULL) {
- rp->path.length = rp->args_start - rp->target_start - 1;
+ if (args != NULL) {
+ rp->path.length = args - rp->target_start - 1;
- rp->args.start = rp->args_start;
- rp->args.length = rp->target_end - rp->args_start;
+ rp->args.length = rp->target_end - args;
+ rp->args.start = args;
} else {
rp->path.length = rp->target_end - rp->target_start;
}
- if (rp->exten_start) {
- rp->exten.length = rp->path.start + rp->path.length
- - rp->exten_start;
- rp->exten.start = rp->exten_start;
+ if (exten != NULL) {
+ rp->exten.length = (rp->path.start + rp->path.length) - exten;
+ rp->exten.start = exten;
}
return nxt_http_parse_field_name(rp, pos, end);
@@ -835,7 +846,8 @@ static const uint8_t nxt_http_normal[32] nxt_aligned(32) = {
static nxt_int_t
nxt_http_parse_complex_target(nxt_http_request_parse_t *rp)
{
- u_char *p, *u, c, ch, high;
+ u_char *p, *u, c, ch, high, *exten, *args;
+
enum {
sw_normal = 0,
sw_slash,
@@ -852,7 +864,6 @@ nxt_http_parse_complex_target(nxt_http_request_parse_t *rp)
p = rp->target_start;
u = nxt_mp_alloc(rp->mem_pool, rp->target_end - p + 1);
-
if (nxt_slow_path(u == NULL)) {
return NXT_ERROR;
}
@@ -861,8 +872,8 @@ nxt_http_parse_complex_target(nxt_http_request_parse_t *rp)
rp->path.start = u;
high = '\0';
- rp->exten_start = NULL;
- rp->args_start = NULL;
+ exten = NULL;
+ args = NULL;
while (p < rp->target_end) {
@@ -881,7 +892,7 @@ nxt_http_parse_complex_target(nxt_http_request_parse_t *rp)
switch (ch) {
case '/':
- rp->exten_start = NULL;
+ exten = NULL;
state = sw_slash;
*u++ = ch;
continue;
@@ -890,17 +901,14 @@ nxt_http_parse_complex_target(nxt_http_request_parse_t *rp)
state = sw_quoted;
continue;
case '?':
- rp->args_start = p;
+ args = p;
goto args;
case '#':
goto done;
case '.':
- rp->exten_start = u + 1;
+ exten = u + 1;
*u++ = ch;
continue;
- case '+':
- rp->plus_in_target = 1;
- /* Fall through. */
default:
*u++ = ch;
continue;
@@ -928,13 +936,10 @@ nxt_http_parse_complex_target(nxt_http_request_parse_t *rp)
state = sw_quoted;
continue;
case '?':
- rp->args_start = p;
+ args = p;
goto args;
case '#':
goto done;
- case '+':
- rp->plus_in_target = 1;
- /* Fall through. */
default:
state = sw_normal;
*u++ = ch;
@@ -965,13 +970,10 @@ nxt_http_parse_complex_target(nxt_http_request_parse_t *rp)
state = sw_quoted;
continue;
case '?':
- rp->args_start = p;
+ args = p;
goto args;
case '#':
goto done;
- case '+':
- rp->plus_in_target = 1;
- /* Fall through. */
default:
state = sw_normal;
*u++ = ch;
@@ -1009,13 +1011,10 @@ nxt_http_parse_complex_target(nxt_http_request_parse_t *rp)
state = sw_quoted;
continue;
case '?':
- rp->args_start = p;
+ args = p;
goto args;
case '#':
goto done;
- case '+':
- rp->plus_in_target = 1;
- /* Fall through. */
default:
state = sw_normal;
*u++ = ch;
@@ -1046,12 +1045,25 @@ nxt_http_parse_complex_target(nxt_http_request_parse_t *rp)
if (ch >= '0' && ch <= '9') {
ch = (u_char) ((high << 4) + ch - '0');
- if (ch == '%' || ch == '#') {
+ if (ch == '%') {
state = sw_normal;
- *u++ = ch;
+ *u++ = '%';
+
+ if (rp->encoded_slashes) {
+ *u++ = '2';
+ *u++ = '5';
+ }
+
continue;
+ }
- } else if (ch == '\0') {
+ if (ch == '#') {
+ state = sw_normal;
+ *u++ = '#';
+ continue;
+ }
+
+ if (ch == '\0') {
return NXT_HTTP_PARSE_INVALID;
}
@@ -1067,9 +1079,14 @@ nxt_http_parse_complex_target(nxt_http_request_parse_t *rp)
state = sw_normal;
*u++ = ch;
continue;
+ }
- } else if (ch == '+') {
- rp->plus_in_target = 1;
+ if (ch == '/' && rp->encoded_slashes) {
+ state = sw_normal;
+ *u++ = '%';
+ *u++ = '2';
+ *u++ = p[-1]; /* 'f' or 'F' */
+ continue;
}
state = saved_state;
@@ -1092,18 +1109,18 @@ args:
}
}
- if (rp->args_start != NULL) {
- rp->args.length = p - rp->args_start;
- rp->args.start = rp->args_start;
+ if (args != NULL) {
+ rp->args.length = p - args;
+ rp->args.start = args;
}
done:
rp->path.length = u - rp->path.start;
- if (rp->exten_start) {
- rp->exten.length = u - rp->exten_start;
- rp->exten.start = rp->exten_start;
+ if (exten) {
+ rp->exten.length = u - exten;
+ rp->exten.start = exten;
}
return NXT_OK;
diff --git a/src/nxt_http_parse.h b/src/nxt_http_parse.h
index c5b11bf3..a307ea73 100644
--- a/src/nxt_http_parse.h
+++ b/src/nxt_http_parse.h
@@ -37,14 +37,10 @@ struct nxt_http_request_parse_s {
nxt_int_t (*handler)(nxt_http_request_parse_t *rp,
u_char **pos, u_char *end);
- size_t offset;
-
nxt_str_t method;
u_char *target_start;
u_char *target_end;
- u_char *exten_start;
- u_char *args_start;
nxt_str_t path;
nxt_str_t args;
@@ -61,13 +57,14 @@ struct nxt_http_request_parse_s {
uint32_t field_hash;
/* target with "/." */
- unsigned complex_target:1;
+ uint8_t complex_target; /* 1 bit */
/* target with "%" */
- unsigned quoted_target:1;
+ uint8_t quoted_target; /* 1 bit */
/* target with " " */
- unsigned space_in_target:1;
- /* target with "+" */
- unsigned plus_in_target:1;
+ uint8_t space_in_target; /* 1 bit */
+
+ /* Preserve encoded '/' (%2F) and '%' (%25). */
+ uint8_t encoded_slashes; /* 1 bit */
};
@@ -113,7 +110,7 @@ nxt_int_t nxt_http_fields_process(nxt_list_t *fields, nxt_lvlhsh_t *hash,
void *ctx);
-const nxt_lvlhsh_proto_t nxt_http_fields_hash_proto;
+extern const nxt_lvlhsh_proto_t nxt_http_fields_hash_proto;
nxt_inline nxt_int_t
nxt_http_field_process(nxt_http_field_t *field, nxt_lvlhsh_t *hash, void *ctx)
diff --git a/src/nxt_http_request.c b/src/nxt_http_request.c
index 916004d2..a18a02e7 100644
--- a/src/nxt_http_request.c
+++ b/src/nxt_http_request.c
@@ -17,8 +17,8 @@ static void nxt_http_request_mem_buf_completion(nxt_task_t *task, void *obj,
void *data);
static void nxt_http_request_done(nxt_task_t *task, void *obj, void *data);
-static u_char *nxt_http_date(u_char *buf, nxt_realtime_t *now, struct tm *tm,
- size_t size, const char *format);
+static u_char *nxt_http_date_cache_handler(u_char *buf, nxt_realtime_t *now,
+ struct tm *tm, size_t size, const char *format);
static const nxt_http_request_state_t nxt_http_request_init_state;
@@ -27,9 +27,9 @@ static const nxt_http_request_state_t nxt_http_request_body_state;
nxt_time_string_t nxt_http_date_cache = {
(nxt_atomic_uint_t) -1,
- nxt_http_date,
- "%s, %02d %s %4d %02d:%02d:%02d GMT",
- nxt_length("Wed, 31 Dec 1986 16:40:00 GMT"),
+ nxt_http_date_cache_handler,
+ NULL,
+ NXT_HTTP_DATE_LEN,
NXT_THREAD_TIME_GMT,
NXT_THREAD_TIME_SEC,
};
@@ -369,7 +369,8 @@ nxt_http_request_read_body(nxt_task_t *task, nxt_http_request_t *r)
void
-nxt_http_request_header_send(nxt_task_t *task, nxt_http_request_t *r)
+nxt_http_request_header_send(nxt_task_t *task, nxt_http_request_t *r,
+ nxt_work_handler_t body_handler)
{
u_char *p, *end;
nxt_http_field_t *server, *date, *content_length;
@@ -430,7 +431,7 @@ nxt_http_request_header_send(nxt_task_t *task, nxt_http_request_t *r)
}
if (nxt_fast_path(r->proto.any != NULL)) {
- nxt_http_proto[r->protocol].header_send(task, r);
+ nxt_http_proto[r->protocol].header_send(task, r, body_handler);
}
return;
@@ -577,17 +578,8 @@ nxt_http_request_close_handler(nxt_task_t *task, void *obj, void *data)
static u_char *
-nxt_http_date(u_char *buf, nxt_realtime_t *now, struct tm *tm, size_t size,
- const char *format)
+nxt_http_date_cache_handler(u_char *buf, nxt_realtime_t *now, struct tm *tm,
+ size_t size, const char *format)
{
- static const char *week[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri",
- "Sat" };
-
- static const char *month[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
- "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
-
- return nxt_sprintf(buf, buf + size, format,
- week[tm->tm_wday], tm->tm_mday,
- month[tm->tm_mon], tm->tm_year + 1900,
- tm->tm_hour, tm->tm_min, tm->tm_sec);
+ return nxt_http_date(buf, tm);
}
diff --git a/src/nxt_http_route.c b/src/nxt_http_route.c
index 0b665573..c3c11faa 100644
--- a/src/nxt_http_route.c
+++ b/src/nxt_http_route.c
@@ -376,15 +376,9 @@ nxt_http_route_match_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf,
nxt_http_route_match_conf_t mtcf;
static nxt_str_t pass_path = nxt_string("/action/pass");
+ static nxt_str_t share_path = nxt_string("/action/share");
static nxt_str_t match_path = nxt_string("/match");
- pass_conf = nxt_conf_get_path(cv, &pass_path);
- if (nxt_slow_path(pass_conf == NULL)) {
- return NULL;
- }
-
- nxt_conf_get_string(pass_conf, &pass);
-
match_conf = nxt_conf_get_path(cv, &match_path);
n = (match_conf != NULL) ? nxt_conf_object_members_count(match_conf) : 0;
@@ -401,6 +395,19 @@ nxt_http_route_match_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf,
match->pass.handler = NULL;
match->items = n;
+ pass_conf = nxt_conf_get_path(cv, &pass_path);
+
+ if (pass_conf == NULL) {
+ pass_conf = nxt_conf_get_path(cv, &share_path);
+ if (nxt_slow_path(pass_conf == NULL)) {
+ return NULL;
+ }
+
+ match->pass.handler = nxt_http_static_handler;
+ }
+
+ nxt_conf_get_string(pass_conf, &pass);
+
string = nxt_str_dup(mp, &match->pass.name, &pass);
if (nxt_slow_path(string == NULL)) {
return NULL;
@@ -870,13 +877,18 @@ static void
nxt_http_route_resolve(nxt_task_t *task, nxt_router_temp_conf_t *tmcf,
nxt_http_route_t *route)
{
+ nxt_http_pass_t *pass;
nxt_http_route_match_t **match, **end;
match = &route->match[0];
end = match + route->items;
while (match < end) {
- nxt_http_pass_resolve(task, tmcf, &(*match)->pass);
+ pass = &(*match)->pass;
+
+ if (pass->handler == NULL) {
+ nxt_http_pass_resolve(task, tmcf, &(*match)->pass);
+ }
match++;
}
diff --git a/src/nxt_http_static.c b/src/nxt_http_static.c
new file mode 100644
index 00000000..44b85389
--- /dev/null
+++ b/src/nxt_http_static.c
@@ -0,0 +1,599 @@
+
+/*
+ * Copyright (C) NGINX, Inc.
+ */
+
+#include <nxt_router.h>
+#include <nxt_http.h>
+
+
+#define NXT_HTTP_STATIC_BUF_COUNT 2
+#define NXT_HTTP_STATIC_BUF_SIZE (128 * 1024)
+
+
+static void nxt_http_static_extract_extension(nxt_str_t *path,
+ nxt_str_t *extension);
+static void nxt_http_static_body_handler(nxt_task_t *task, void *obj,
+ void *data);
+static void nxt_http_static_buf_completion(nxt_task_t *task, void *obj,
+ void *data);
+
+static nxt_int_t nxt_http_static_mtypes_hash_test(nxt_lvlhsh_query_t *lhq,
+ void *data);
+static void *nxt_http_static_mtypes_hash_alloc(void *data, size_t size);
+static void nxt_http_static_mtypes_hash_free(void *data, void *p);
+
+
+static const nxt_http_request_state_t nxt_http_static_send_state;
+
+
+nxt_http_pass_t *
+nxt_http_static_handler(nxt_task_t *task, nxt_http_request_t *r,
+ nxt_http_pass_t *pass)
+{
+ size_t alloc, encode;
+ u_char *p;
+ struct tm tm;
+ nxt_buf_t *fb;
+ nxt_int_t ret;
+ nxt_str_t index, extension, *mtype;
+ nxt_uint_t level;
+ nxt_bool_t need_body;
+ nxt_file_t *f;
+ nxt_file_info_t fi;
+ nxt_http_field_t *field;
+ nxt_http_status_t status;
+ nxt_router_conf_t *rtcf;
+ nxt_work_handler_t body_handler;
+
+ if (nxt_slow_path(!nxt_str_eq(r->method, "GET", 3))) {
+
+ if (!nxt_str_eq(r->method, "HEAD", 4)) {
+ nxt_http_request_error(task, r, NXT_HTTP_METHOD_NOT_ALLOWED);
+ return NULL;
+ }
+
+ need_body = 0;
+
+ } else {
+ need_body = 1;
+ }
+
+ f = nxt_mp_zget(r->mem_pool, sizeof(nxt_file_t));
+ if (nxt_slow_path(f == NULL)) {
+ goto fail;
+ }
+
+ f->fd = NXT_FILE_INVALID;
+
+ if (r->path->start[r->path->length - 1] == '/') {
+ /* TODO: dynamic index setting. */
+ nxt_str_set(&index, "index.html");
+ nxt_str_set(&extension, ".html");
+
+ } else {
+ nxt_str_null(&index);
+ nxt_str_null(&extension);
+ }
+
+ alloc = pass->name.length + r->path->length + index.length + 1;
+
+ f->name = nxt_mp_nget(r->mem_pool, alloc);
+ if (nxt_slow_path(f->name == NULL)) {
+ goto fail;
+ }
+
+ p = f->name;
+ p = nxt_cpymem(p, pass->name.start, pass->name.length);
+ p = nxt_cpymem(p, r->path->start, r->path->length);
+ p = nxt_cpymem(p, index.start, index.length);
+ *p = '\0';
+
+ ret = nxt_file_open(task, f, NXT_FILE_RDONLY, NXT_FILE_OPEN, 0);
+
+ if (nxt_slow_path(ret != NXT_OK)) {
+ switch (f->error) {
+
+ case NXT_ENOENT:
+ case NXT_ENOTDIR:
+ case NXT_ENAMETOOLONG:
+ level = NXT_LOG_ERR;
+ status = NXT_HTTP_NOT_FOUND;
+ break;
+
+ case NXT_EACCES:
+ level = NXT_LOG_ERR;
+ status = NXT_HTTP_FORBIDDEN;
+ break;
+
+ default:
+ level = NXT_LOG_ALERT;
+ status = NXT_HTTP_INTERNAL_SERVER_ERROR;
+ break;
+ }
+
+ if (status != NXT_HTTP_NOT_FOUND) {
+ nxt_log(task, level, "open(\"%FN\") failed %E", f->name, f->error);
+ }
+
+ nxt_http_request_error(task, r, status);
+ return NULL;
+ }
+
+ ret = nxt_file_info(f, &fi);
+ if (nxt_slow_path(ret != NXT_OK)) {
+ goto fail;
+ }
+
+ if (nxt_fast_path(nxt_is_file(&fi))) {
+ r->status = NXT_HTTP_OK;
+ r->resp.content_length_n = nxt_file_size(&fi);
+
+ field = nxt_list_zero_add(r->resp.fields);
+ if (nxt_slow_path(field == NULL)) {
+ goto fail;
+ }
+
+ nxt_http_field_name_set(field, "Last-Modified");
+
+ p = nxt_mp_nget(r->mem_pool, NXT_HTTP_DATE_LEN);
+ if (nxt_slow_path(p == NULL)) {
+ goto fail;
+ }
+
+ nxt_localtime(nxt_file_mtime(&fi), &tm);
+
+ field->value = p;
+ field->value_length = nxt_http_date(p, &tm) - p;
+
+ field = nxt_list_zero_add(r->resp.fields);
+ if (nxt_slow_path(field == NULL)) {
+ goto fail;
+ }
+
+ nxt_http_field_name_set(field, "ETag");
+
+ alloc = NXT_TIME_T_HEXLEN + NXT_OFF_T_HEXLEN + 3;
+
+ p = nxt_mp_nget(r->mem_pool, alloc);
+ if (nxt_slow_path(p == NULL)) {
+ goto fail;
+ }
+
+ field->value = p;
+ field->value_length = nxt_sprintf(p, p + alloc, "\"%xT-%xO\"",
+ nxt_file_mtime(&fi),
+ nxt_file_size(&fi))
+ - p;
+
+ if (extension.start == NULL) {
+ nxt_http_static_extract_extension(r->path, &extension);
+ }
+
+ rtcf = r->conf->socket_conf->router_conf;
+
+ mtype = nxt_http_static_mtypes_hash_find(&rtcf->mtypes_hash,
+ &extension);
+
+ if (mtype != NULL) {
+ field = nxt_list_zero_add(r->resp.fields);
+ if (nxt_slow_path(field == NULL)) {
+ goto fail;
+ }
+
+ nxt_http_field_name_set(field, "Content-Type");
+
+ field->value = mtype->start;
+ field->value_length = mtype->length;
+ }
+
+ if (need_body && nxt_file_size(&fi) > 0) {
+ fb = nxt_mp_zget(r->mem_pool, NXT_BUF_FILE_SIZE);
+ if (nxt_slow_path(fb == NULL)) {
+ goto fail;
+ }
+
+ fb->file = f;
+ fb->file_end = nxt_file_size(&fi);
+
+ r->out = fb;
+
+ body_handler = &nxt_http_static_body_handler;
+
+ } else {
+ nxt_file_close(task, f);
+ body_handler = NULL;
+ }
+
+ } else {
+ /* Not a file. */
+
+ nxt_file_close(task, f);
+ f = NULL;
+
+ if (nxt_slow_path(!nxt_is_dir(&fi))) {
+ nxt_log(task, NXT_LOG_ERR, "\"%FN\" is not a regular file",
+ f->name);
+ nxt_http_request_error(task, r, NXT_HTTP_NOT_FOUND);
+ return NULL;
+ }
+
+ r->status = NXT_HTTP_MOVED_PERMANENTLY;
+ r->resp.content_length_n = 0;
+
+ field = nxt_list_zero_add(r->resp.fields);
+ if (nxt_slow_path(field == NULL)) {
+ goto fail;
+ }
+
+ nxt_http_field_name_set(field, "Location");
+
+ encode = nxt_encode_uri(NULL, r->path->start, r->path->length);
+ alloc = r->path->length + encode * 2 + 1;
+
+ if (r->args->length > 0) {
+ alloc += 1 + r->args->length;
+ }
+
+ p = nxt_mp_nget(r->mem_pool, alloc);
+ if (nxt_slow_path(p == NULL)) {
+ goto fail;
+ }
+
+ field->value = p;
+ field->value_length = alloc;
+
+ if (encode > 0) {
+ p = (u_char *) nxt_encode_uri(p, r->path->start, r->path->length);
+
+ } else {
+ p = nxt_cpymem(p, r->path->start, r->path->length);
+ }
+
+ *p++ = '/';
+
+ if (r->args->length > 0) {
+ *p++ = '?';
+ nxt_memcpy(p, r->args->start, r->args->length);
+ }
+
+ body_handler = NULL;
+ }
+
+ nxt_http_request_header_send(task, r, body_handler);
+
+ r->state = &nxt_http_static_send_state;
+ return NULL;
+
+fail:
+
+ nxt_http_request_error(task, r, NXT_HTTP_INTERNAL_SERVER_ERROR);
+
+ if (f != NULL && f->fd != NXT_FILE_INVALID) {
+ nxt_file_close(task, f);
+ }
+
+ return NULL;
+}
+
+
+static void
+nxt_http_static_extract_extension(nxt_str_t *path, nxt_str_t *extension)
+{
+ u_char ch, *p, *end;
+
+ end = path->start + path->length;
+ p = end;
+
+ for ( ;; ) {
+ /* There's always '/' in the beginning of the request path. */
+
+ p--;
+ ch = *p;
+
+ switch (ch) {
+ case '/':
+ p++;
+ /* Fall through. */
+ case '.':
+ extension->length = end - p;
+ extension->start = p;
+ return;
+ }
+ }
+}
+
+
+static void
+nxt_http_static_body_handler(nxt_task_t *task, void *obj, void *data)
+{
+ size_t alloc;
+ nxt_buf_t *fb, *b, **next, *out;
+ nxt_off_t rest;
+ nxt_int_t n;
+ nxt_work_queue_t *wq;
+ nxt_http_request_t *r;
+
+ r = obj;
+ fb = r->out;
+
+ rest = fb->file_end - fb->file_pos;
+ out = NULL;
+ next = &out;
+ n = 0;
+
+ do {
+ alloc = nxt_min(rest, NXT_HTTP_STATIC_BUF_SIZE);
+
+ b = nxt_buf_mem_alloc(r->mem_pool, alloc, 0);
+ if (nxt_slow_path(b == NULL)) {
+ goto fail;
+ }
+
+ b->completion_handler = nxt_http_static_buf_completion;
+ b->parent = r;
+
+ nxt_mp_retain(r->mem_pool);
+
+ *next = b;
+ next = &b->next;
+
+ rest -= alloc;
+
+ } while (rest > 0 && ++n < NXT_HTTP_STATIC_BUF_COUNT);
+
+ wq = &task->thread->engine->fast_work_queue;
+
+ nxt_sendbuf_drain(task, wq, out);
+ return;
+
+fail:
+
+ while (out != NULL) {
+ b = out;
+ out = b->next;
+
+ nxt_mp_free(r->mem_pool, b);
+ nxt_mp_release(r->mem_pool);
+ }
+}
+
+
+static const nxt_http_request_state_t nxt_http_static_send_state
+ nxt_aligned(64) =
+{
+ .error_handler = nxt_http_request_error_handler,
+};
+
+
+static void
+nxt_http_static_buf_completion(nxt_task_t *task, void *obj, void *data)
+{
+ ssize_t n, size;
+ nxt_buf_t *b, *fb;
+ nxt_off_t rest;
+ nxt_http_request_t *r;
+
+ b = obj;
+ r = data;
+ fb = r->out;
+
+ if (nxt_slow_path(fb == NULL || r->error)) {
+ goto clean;
+ }
+
+ rest = fb->file_end - fb->file_pos;
+ size = nxt_buf_mem_size(&b->mem);
+
+ size = nxt_min(rest, (nxt_off_t) size);
+
+ n = nxt_file_read(fb->file, b->mem.start, size, fb->file_pos);
+
+ if (n != size) {
+ if (n >= 0) {
+ nxt_log(task, NXT_LOG_ERR, "file \"%FN\" has changed "
+ "while sending response to a client", fb->file->name);
+ }
+
+ nxt_http_request_error_handler(task, r, r->proto.any);
+ goto clean;
+ }
+
+ if (n == rest) {
+ nxt_file_close(task, fb->file);
+ r->out = NULL;
+
+ b->next = nxt_http_buf_last(r);
+
+ } else {
+ fb->file_pos += n;
+ b->next = NULL;
+ }
+
+ b->mem.pos = b->mem.start;
+ b->mem.free = b->mem.pos + n;
+
+ nxt_http_request_send(task, r, b);
+ return;
+
+clean:
+
+ nxt_mp_free(r->mem_pool, b);
+ nxt_mp_release(r->mem_pool);
+
+ if (fb != NULL) {
+ nxt_file_close(task, fb->file);
+ r->out = NULL;
+ }
+}
+
+
+nxt_int_t
+nxt_http_static_mtypes_init(nxt_mp_t *mp, nxt_lvlhsh_t *hash)
+{
+ nxt_str_t *type, extension;
+ nxt_int_t ret;
+ nxt_uint_t i;
+
+ static const struct {
+ nxt_str_t type;
+ const char *extension;
+ } default_types[] = {
+
+ { nxt_string("text/html"), ".html" },
+ { nxt_string("text/html"), ".htm" },
+ { nxt_string("text/css"), ".css" },
+
+ { nxt_string("image/svg+xml"), ".svg" },
+ { nxt_string("image/svg+xml"), ".svg" },
+ { nxt_string("image/webp"), ".webp" },
+ { nxt_string("image/png"), ".png" },
+ { nxt_string("image/jpeg"), ".jpeg" },
+ { nxt_string("image/jpeg"), ".jpg" },
+ { nxt_string("image/gif"), ".gif" },
+ { nxt_string("image/x-icon"), ".ico" },
+
+ { nxt_string("font/woff"), ".woff" },
+ { nxt_string("font/woff2"), ".woff2" },
+ { nxt_string("font/otf"), ".otf" },
+ { nxt_string("font/ttf"), ".ttf" },
+
+ { nxt_string("text/plain"), ".txt" },
+ { nxt_string("text/markdown"), ".md" },
+ { nxt_string("text/x-rst"), ".rst" },
+
+ { nxt_string("application/javascript"), ".js" },
+ { nxt_string("application/json"), ".json" },
+ { nxt_string("application/xml"), ".xml" },
+ { nxt_string("application/rss+xml"), ".rss" },
+ { nxt_string("application/atom+xml"), ".atom" },
+ { nxt_string("application/pdf"), ".pdf" },
+
+ { nxt_string("application/zip"), ".zip" },
+
+ { nxt_string("audio/mpeg"), ".mp3" },
+ { nxt_string("audio/ogg"), ".ogg" },
+ { nxt_string("audio/midi"), ".midi" },
+ { nxt_string("audio/midi"), ".mid" },
+ { nxt_string("audio/flac"), ".flac" },
+ { nxt_string("audio/aac"), ".aac" },
+ { nxt_string("audio/wav"), ".wav" },
+
+ { nxt_string("video/mpeg"), ".mpeg" },
+ { nxt_string("video/mpeg"), ".mpg" },
+ { nxt_string("video/mp4"), ".mp4" },
+ { nxt_string("video/webm"), ".webm" },
+ { nxt_string("video/x-msvideo"), ".avi" },
+
+ { nxt_string("application/octet-stream"), ".exe" },
+ { nxt_string("application/octet-stream"), ".bin" },
+ { nxt_string("application/octet-stream"), ".dll" },
+ { nxt_string("application/octet-stream"), ".iso" },
+ { nxt_string("application/octet-stream"), ".img" },
+ { nxt_string("application/octet-stream"), ".msi" },
+
+ { nxt_string("application/octet-stream"), ".deb" },
+ { nxt_string("application/octet-stream"), ".rpm" },
+ };
+
+ for (i = 0; i < nxt_nitems(default_types); i++) {
+ type = (nxt_str_t *) &default_types[i].type;
+
+ extension.start = (u_char *) default_types[i].extension;
+ extension.length = nxt_strlen(extension.start);
+
+ ret = nxt_http_static_mtypes_hash_add(mp, hash, &extension, type);
+ if (nxt_slow_path(ret != NXT_OK)) {
+ return NXT_ERROR;
+ }
+ }
+
+ return NXT_OK;
+}
+
+
+static const nxt_lvlhsh_proto_t nxt_http_static_mtypes_hash_proto
+ nxt_aligned(64) =
+{
+ NXT_LVLHSH_DEFAULT,
+ nxt_http_static_mtypes_hash_test,
+ nxt_http_static_mtypes_hash_alloc,
+ nxt_http_static_mtypes_hash_free,
+};
+
+
+typedef struct {
+ nxt_str_t extension;
+ nxt_str_t *type;
+} nxt_http_static_mtype_t;
+
+
+nxt_int_t
+nxt_http_static_mtypes_hash_add(nxt_mp_t *mp, nxt_lvlhsh_t *hash,
+ nxt_str_t *extension, nxt_str_t *type)
+{
+ nxt_lvlhsh_query_t lhq;
+ nxt_http_static_mtype_t *mtype;
+
+ mtype = nxt_mp_get(mp, sizeof(nxt_http_static_mtype_t));
+ if (nxt_slow_path(mtype == NULL)) {
+ return NXT_ERROR;
+ }
+
+ mtype->extension = *extension;
+ mtype->type = type;
+
+ lhq.key = *extension;
+ lhq.key_hash = nxt_djb_hash_lowcase(lhq.key.start, lhq.key.length);
+ lhq.replace = 1;
+ lhq.value = mtype;
+ lhq.proto = &nxt_http_static_mtypes_hash_proto;
+ lhq.pool = mp;
+
+ return nxt_lvlhsh_insert(hash, &lhq);
+}
+
+
+nxt_str_t *
+nxt_http_static_mtypes_hash_find(nxt_lvlhsh_t *hash, nxt_str_t *extension)
+{
+ nxt_lvlhsh_query_t lhq;
+ nxt_http_static_mtype_t *mtype;
+
+ lhq.key = *extension;
+ lhq.key_hash = nxt_djb_hash_lowcase(lhq.key.start, lhq.key.length);
+ lhq.proto = &nxt_http_static_mtypes_hash_proto;
+
+ if (nxt_lvlhsh_find(hash, &lhq) == NXT_OK) {
+ mtype = lhq.value;
+ return mtype->type;
+ }
+
+ return NULL;
+}
+
+
+static nxt_int_t
+nxt_http_static_mtypes_hash_test(nxt_lvlhsh_query_t *lhq, void *data)
+{
+ nxt_http_static_mtype_t *mtype;
+
+ mtype = data;
+
+ return nxt_strcasestr_eq(&lhq->key, &mtype->extension) ? NXT_OK
+ : NXT_DECLINED;
+}
+
+
+static void *
+nxt_http_static_mtypes_hash_alloc(void *data, size_t size)
+{
+ return nxt_mp_align(data, size, size);
+}
+
+
+static void
+nxt_http_static_mtypes_hash_free(void *data, void *p)
+{
+ nxt_mp_free(data, p);
+}
diff --git a/src/nxt_java.c b/src/nxt_java.c
index 3421d825..08e24595 100644
--- a/src/nxt_java.c
+++ b/src/nxt_java.c
@@ -13,6 +13,7 @@
#include <nxt_unit_field.h>
#include <nxt_unit_request.h>
#include <nxt_unit_response.h>
+#include <nxt_unit_websocket.h>
#include <java/nxt_jni.h>
@@ -30,6 +31,8 @@ static nxt_int_t nxt_java_pre_init(nxt_task_t *task,
nxt_common_app_conf_t *conf);
static nxt_int_t nxt_java_init(nxt_task_t *task, nxt_common_app_conf_t *conf);
static void nxt_java_request_handler(nxt_unit_request_info_t *req);
+static void nxt_java_websocket_handler(nxt_unit_websocket_frame_t *ws);
+static void nxt_java_close_handler(nxt_unit_request_info_t *req);
static uint32_t compat[] = {
NXT_VERNUM, NXT_DEBUG,
@@ -347,6 +350,8 @@ nxt_java_init(nxt_task_t *task, nxt_common_app_conf_t *conf)
nxt_unit_default_init(task, &java_init);
java_init.callbacks.request_handler = nxt_java_request_handler;
+ java_init.callbacks.websocket_handler = nxt_java_websocket_handler;
+ java_init.callbacks.close_handler = nxt_java_close_handler;
java_init.request_data_size = sizeof(nxt_java_request_data_t);
java_init.data = &data;
@@ -361,14 +366,14 @@ nxt_java_init(nxt_task_t *task, nxt_common_app_conf_t *conf)
/* TODO report error */
}
- nxt_unit_done(ctx);
-
nxt_java_stopContext(env, data.ctx);
if ((*env)->ExceptionCheck(env)) {
(*env)->ExceptionDescribe(env);
}
+ nxt_unit_done(ctx);
+
(*jvm)->DestroyJavaVM(jvm);
exit(0);
@@ -454,8 +459,71 @@ nxt_java_request_handler(nxt_unit_request_info_t *req)
data->buf = NULL;
}
+ if (nxt_unit_response_is_websocket(req)) {
+ data->jreq = (*env)->NewGlobalRef(env, jreq);
+ data->jresp = (*env)->NewGlobalRef(env, jresp);
+
+ } else {
+ nxt_unit_request_done(req, NXT_UNIT_OK);
+ }
+
(*env)->DeleteLocalRef(env, jresp);
(*env)->DeleteLocalRef(env, jreq);
+}
+
+
+static void
+nxt_java_websocket_handler(nxt_unit_websocket_frame_t *ws)
+{
+ void *b;
+ JNIEnv *env;
+ jobject jbuf;
+ nxt_java_data_t *java_data;
+ nxt_java_request_data_t *data;
+
+ java_data = ws->req->unit->data;
+ env = java_data->env;
+ data = ws->req->data;
+
+ b = malloc(ws->payload_len);
+ if (b != NULL) {
+ nxt_unit_websocket_read(ws, b, ws->payload_len);
+
+ jbuf = (*env)->NewDirectByteBuffer(env, b, ws->payload_len);
+ if (jbuf != NULL) {
+ nxt_java_Request_websocket(env, data->jreq, jbuf,
+ ws->header->opcode, ws->header->fin);
+
+ if ((*env)->ExceptionCheck(env)) {
+ (*env)->ExceptionDescribe(env);
+ (*env)->ExceptionClear(env);
+ }
+
+ (*env)->DeleteLocalRef(env, jbuf);
+ }
+
+ free(b);
+ }
+
+ nxt_unit_websocket_done(ws);
+}
+
+
+static void
+nxt_java_close_handler(nxt_unit_request_info_t *req)
+{
+ JNIEnv *env;
+ nxt_java_data_t *java_data;
+ nxt_java_request_data_t *data;
+
+ java_data = req->unit->data;
+ env = java_data->env;
+ data = req->data;
+
+ nxt_java_Request_close(env, data->jreq);
+
+ (*env)->DeleteGlobalRef(env, data->jresp);
+ (*env)->DeleteGlobalRef(env, data->jreq);
nxt_unit_request_done(req, NXT_UNIT_OK);
}
diff --git a/src/nxt_main.h b/src/nxt_main.h
index 23c55002..0afebb96 100644
--- a/src/nxt_main.h
+++ b/src/nxt_main.h
@@ -57,6 +57,7 @@ typedef uint16_t nxt_port_id_t;
#include <nxt_fiber.h>
#include <nxt_thread.h>
#include <nxt_process_type.h>
+#include <nxt_capability.h>
#include <nxt_process.h>
#include <nxt_utf8.h>
#include <nxt_file_name.h>
diff --git a/src/nxt_main_process.c b/src/nxt_main_process.c
index 40682eb9..44deb272 100644
--- a/src/nxt_main_process.c
+++ b/src/nxt_main_process.c
@@ -14,6 +14,10 @@
#include <nxt_cert.h>
#endif
+#ifdef NXT_LINUX
+#include <linux/sched.h>
+#endif
+
typedef struct {
nxt_socket_t socket;
@@ -68,6 +72,10 @@ static void nxt_main_port_conf_store_handler(nxt_task_t *task,
static void nxt_main_port_access_log_handler(nxt_task_t *task,
nxt_port_recv_msg_t *msg);
+static nxt_int_t nxt_init_set_isolation(nxt_task_t *task,
+ nxt_process_init_t *init, nxt_conf_value_t *isolation);
+static nxt_int_t nxt_init_set_ns(nxt_task_t *task,
+ nxt_process_init_t *init, nxt_conf_value_t *ns);
const nxt_sig_event_t nxt_main_process_signals[] = {
nxt_event_signal(SIGHUP, nxt_main_process_signal_handler),
@@ -134,6 +142,12 @@ static nxt_conf_map_t nxt_common_app_conf[] = {
NXT_CONF_MAP_PTR,
offsetof(nxt_common_app_conf_t, environment),
},
+
+ {
+ nxt_string("isolation"),
+ NXT_CONF_MAP_PTR,
+ offsetof(nxt_common_app_conf_t, isolation),
+ }
};
@@ -271,12 +285,11 @@ nxt_port_main_start_worker_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg)
nxt_int_t ret;
nxt_buf_t *b;
nxt_port_t *port;
+ nxt_runtime_t *rt;
nxt_app_type_t idx;
nxt_conf_value_t *conf;
nxt_common_app_conf_t app_conf;
- static nxt_str_t nobody = nxt_string("nobody");
-
ret = NXT_ERROR;
mp = nxt_mp_create(1024, 128, 256, 32);
@@ -311,7 +324,10 @@ nxt_port_main_start_worker_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg)
goto failed;
}
- app_conf.user = nobody;
+ rt = task->thread->runtime;
+
+ app_conf.user.start = (u_char*)rt->user_cred.user;
+ app_conf.user.length = nxt_strlen(rt->user_cred.user);
ret = nxt_conf_map_object(mp, conf, nxt_common_app_conf,
nxt_nitems(nxt_common_app_conf), &app_conf);
@@ -458,6 +474,8 @@ nxt_main_start_controller_process(nxt_task_t *task, nxt_runtime_t *rt)
return NXT_ERROR;
}
+ nxt_memzero(init, sizeof(nxt_process_init_t));
+
init->start = nxt_controller_start;
init->name = "controller";
init->user_cred = &rt->user_cred;
@@ -552,6 +570,8 @@ nxt_main_start_discovery_process(nxt_task_t *task, nxt_runtime_t *rt)
return NXT_ERROR;
}
+ nxt_memzero(init, sizeof(nxt_process_init_t));
+
init->start = nxt_discovery_start;
init->name = "discovery";
init->user_cred = &rt->user_cred;
@@ -576,6 +596,8 @@ nxt_main_start_router_process(nxt_task_t *task, nxt_runtime_t *rt)
return NXT_ERROR;
}
+ nxt_memzero(init, sizeof(nxt_process_init_t));
+
init->start = nxt_router_start;
init->name = "router";
init->user_cred = &rt->user_cred;
@@ -589,7 +611,6 @@ nxt_main_start_router_process(nxt_task_t *task, nxt_runtime_t *rt)
return nxt_main_create_worker_process(task, rt, init);
}
-
static nxt_int_t
nxt_main_start_worker_process(nxt_task_t *task, nxt_runtime_t *rt,
nxt_common_app_conf_t *app_conf, uint32_t stream)
@@ -597,41 +618,72 @@ nxt_main_start_worker_process(nxt_task_t *task, nxt_runtime_t *rt,
char *user, *group;
u_char *title, *last, *end;
size_t size;
+ nxt_int_t ret;
nxt_process_init_t *init;
size = sizeof(nxt_process_init_t)
- + sizeof(nxt_user_cred_t)
- + app_conf->user.length + 1
- + app_conf->group.length + 1
- + app_conf->name.length + sizeof("\"\" application");
+ + app_conf->name.length
+ + sizeof("\"\" application");
+
+ if (rt->capabilities.setid) {
+ size += sizeof(nxt_user_cred_t)
+ + app_conf->user.length + 1
+ + app_conf->group.length + 1;
+ }
init = nxt_malloc(size);
if (nxt_slow_path(init == NULL)) {
return NXT_ERROR;
}
- init->user_cred = nxt_pointer_to(init, sizeof(nxt_process_init_t));
- user = nxt_pointer_to(init->user_cred, sizeof(nxt_user_cred_t));
+ nxt_memzero(init, sizeof(nxt_process_init_t));
- nxt_memcpy(user, app_conf->user.start, app_conf->user.length);
- last = nxt_pointer_to(user, app_conf->user.length);
- *last++ = '\0';
+ if (rt->capabilities.setid) {
+ init->user_cred = nxt_pointer_to(init, sizeof(nxt_process_init_t));
+ user = nxt_pointer_to(init->user_cred, sizeof(nxt_user_cred_t));
- init->user_cred->user = user;
+ nxt_memcpy(user, app_conf->user.start, app_conf->user.length);
+ last = nxt_pointer_to(user, app_conf->user.length);
+ *last++ = '\0';
- if (app_conf->group.start != NULL) {
- group = (char *) last;
+ init->user_cred->user = user;
- nxt_memcpy(group, app_conf->group.start, app_conf->group.length);
- last = nxt_pointer_to(group, app_conf->group.length);
- *last++ = '\0';
+ if (app_conf->group.start != NULL) {
+ group = (char *) last;
+
+ nxt_memcpy(group, app_conf->group.start, app_conf->group.length);
+ last = nxt_pointer_to(group, app_conf->group.length);
+ *last++ = '\0';
+
+ } else {
+ group = NULL;
+ }
+
+ ret = nxt_user_cred_get(task, init->user_cred, group);
+ if (ret != NXT_OK) {
+ return NXT_ERROR;
+ }
} else {
- group = NULL;
- }
+ if (!nxt_str_eq(&app_conf->user, (u_char *) rt->user_cred.user,
+ nxt_strlen(rt->user_cred.user)))
+ {
+ nxt_alert(task, "cannot set user \"%V\" for app \"%V\": "
+ "missing capabilities", &app_conf->user, &app_conf->name);
+ return NXT_ERROR;
+ }
- if (nxt_user_cred_get(task, init->user_cred, group) != NXT_OK) {
- return NXT_ERROR;
+ if (app_conf->group.length > 0
+ && !nxt_str_eq(&app_conf->group, (u_char *) rt->group,
+ nxt_strlen(rt->group)))
+ {
+ nxt_alert(task, "cannot set group \"%V\" for app \"%V\": "
+ "missing capabilities", &app_conf->group,
+ &app_conf->name);
+ return NXT_ERROR;
+ }
+
+ last = nxt_pointer_to(init, sizeof(nxt_process_init_t));
}
title = last;
@@ -648,6 +700,11 @@ nxt_main_start_worker_process(nxt_task_t *task, nxt_runtime_t *rt,
init->stream = stream;
init->restart = NULL;
+ ret = nxt_init_set_isolation(task, init, app_conf->isolation);
+ if (nxt_slow_path(ret != NXT_OK)) {
+ return NXT_ERROR;
+ }
+
return nxt_main_create_worker_process(task, rt, init);
}
@@ -690,15 +747,18 @@ nxt_main_create_worker_process(nxt_task_t *task, nxt_runtime_t *rt,
pid = nxt_process_create(task, process);
- nxt_port_use(task, port, -1);
-
switch (pid) {
case -1:
+ nxt_port_close(task, port);
+ nxt_port_use(task, port, -1);
+
return NXT_ERROR;
case 0:
/* A worker process, return to the event engine work queue loop. */
+ nxt_port_use(task, port, -1);
+
return NXT_AGAIN;
default:
@@ -707,6 +767,8 @@ nxt_main_create_worker_process(nxt_task_t *task, nxt_runtime_t *rt,
nxt_port_read_close(port);
nxt_port_write_enable(task, port);
+ nxt_port_use(task, port, -1);
+
return NXT_OK;
}
}
@@ -1241,7 +1303,7 @@ nxt_main_port_modules_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg)
nxt_conf_value_t *conf, *root, *value;
nxt_app_lang_module_t *lang;
- static nxt_str_t root_path = nxt_string("/");
+ static nxt_str_t root_path = nxt_string("/");
rt = task->thread->runtime;
@@ -1433,3 +1495,105 @@ nxt_main_port_access_log_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg)
msg->port_msg.stream, 0, NULL);
}
}
+
+
+static nxt_int_t
+nxt_init_set_isolation(nxt_task_t *task, nxt_process_init_t *init,
+ nxt_conf_value_t *isolation)
+{
+ nxt_int_t ret;
+ nxt_conf_value_t *object;
+
+ static nxt_str_t nsname = nxt_string("namespaces");
+ static nxt_str_t uidname = nxt_string("uidmap");
+ static nxt_str_t gidname = nxt_string("gidmap");
+
+ if (isolation == NULL) {
+ return NXT_OK;
+ }
+
+ object = nxt_conf_get_object_member(isolation, &nsname, NULL);
+ if (object != NULL) {
+ ret = nxt_init_set_ns(task, init, object);
+ if (ret != NXT_OK) {
+ return ret;
+ }
+ }
+
+ object = nxt_conf_get_object_member(isolation, &uidname, NULL);
+ if (object != NULL) {
+ init->isolation.clone.uidmap = object;
+ }
+
+ object = nxt_conf_get_object_member(isolation, &gidname, NULL);
+ if (object != NULL) {
+ init->isolation.clone.gidmap = object;
+ }
+
+ return NXT_OK;
+}
+
+
+static nxt_int_t
+nxt_init_set_ns(nxt_task_t *task, nxt_process_init_t *init, nxt_conf_value_t *namespaces)
+{
+ uint32_t index;
+ nxt_str_t name;
+ nxt_int_t flag;
+ nxt_conf_value_t *value;
+
+ index = 0;
+
+ while ((value = nxt_conf_next_object_member(namespaces, &name, &index)) != NULL) {
+ flag = 0;
+
+#if (NXT_HAVE_CLONE_NEWUSER)
+ if (nxt_str_eq(&name, "credential", 10)) {
+ flag = CLONE_NEWUSER;
+ }
+#endif
+
+#if (NXT_HAVE_CLONE_NEWPID)
+ if (nxt_str_eq(&name, "pid", 3)) {
+ flag = CLONE_NEWPID;
+ }
+#endif
+
+#if (NXT_HAVE_CLONE_NEWNET)
+ if (nxt_str_eq(&name, "network", 7)) {
+ flag = CLONE_NEWNET;
+ }
+#endif
+
+#if (NXT_HAVE_CLONE_NEWUTS)
+ if (nxt_str_eq(&name, "uname", 5)) {
+ flag = CLONE_NEWUTS;
+ }
+#endif
+
+#if (NXT_HAVE_CLONE_NEWNS)
+ if (nxt_str_eq(&name, "mount", 5)) {
+ flag = CLONE_NEWNS;
+ }
+#endif
+
+#if (NXT_HAVE_CLONE_NEWCGROUP)
+ if (nxt_str_eq(&name, "cgroup", 6)) {
+ flag = CLONE_NEWCGROUP;
+ }
+#endif
+
+ if (!flag) {
+ nxt_alert(task, "unknown namespace flag: \"%V\"", &name);
+ return NXT_ERROR;
+ }
+
+ if (nxt_conf_get_integer(value) == 0) {
+ continue; /* process shares everything by default */
+ }
+
+ init->isolation.clone.flags |= flag;
+ }
+
+ return NXT_OK;
+}
diff --git a/src/nxt_port.c b/src/nxt_port.c
index cef65cab..9029353a 100644
--- a/src/nxt_port.c
+++ b/src/nxt_port.c
@@ -546,7 +546,7 @@ nxt_port_use(nxt_task_t *task, nxt_port_t *port, int i)
if (i < 0 && c == -i) {
- if (task->thread->engine == port->engine) {
+ if (port->engine == NULL || task->thread->engine == port->engine) {
nxt_port_release(task, port);
return;
diff --git a/src/nxt_process.c b/src/nxt_process.c
index c4aef21c..638765a4 100644
--- a/src/nxt_process.c
+++ b/src/nxt_process.c
@@ -7,10 +7,16 @@
#include <nxt_main.h>
#include <nxt_main_process.h>
+#if (NXT_HAVE_CLONE)
+#include <nxt_clone.h>
+#endif
+
+#include <signal.h>
static void nxt_process_start(nxt_task_t *task, nxt_process_t *process);
static nxt_int_t nxt_user_groups_get(nxt_task_t *task, nxt_user_cred_t *uc);
-
+static nxt_int_t nxt_process_worker_setup(nxt_task_t *task,
+ nxt_process_t *process, int parentfd);
/* A cached process pid. */
nxt_pid_t nxt_pid;
@@ -34,84 +40,217 @@ nxt_bool_t nxt_proc_remove_notify_matrix[NXT_PROCESS_MAX][NXT_PROCESS_MAX] = {
{ 0, 0, 0, 1, 0 },
};
-nxt_pid_t
-nxt_process_create(nxt_task_t *task, nxt_process_t *process)
-{
- nxt_pid_t pid;
+
+static nxt_int_t
+nxt_process_worker_setup(nxt_task_t *task, nxt_process_t *process, int parentfd) {
+ pid_t rpid, pid;
+ ssize_t n;
+ nxt_int_t parent_status;
nxt_process_t *p;
nxt_runtime_t *rt;
+ nxt_process_init_t *init;
nxt_process_type_t ptype;
- rt = task->thread->runtime;
+ pid = getpid();
+ rpid = 0;
+ rt = task->thread->runtime;
+ init = process->init;
- pid = fork();
+ /* Setup the worker process. */
- switch (pid) {
+ n = read(parentfd, &rpid, sizeof(rpid));
+ if (nxt_slow_path(n == -1 || n != sizeof(rpid))) {
+ nxt_alert(task, "failed to read real pid");
+ return NXT_ERROR;
+ }
- case -1:
- nxt_alert(task, "fork() failed while creating \"%s\" %E",
- process->init->name, nxt_errno);
- break;
+ if (nxt_slow_path(rpid == 0)) {
+ nxt_alert(task, "failed to get real pid from parent");
+ return NXT_ERROR;
+ }
- case 0:
- /* A child. */
- nxt_pid = getpid();
+ nxt_pid = rpid;
+
+ /* Clean inherited cached thread tid. */
+ task->thread->tid = 0;
+
+ process->pid = nxt_pid;
+
+ if (nxt_pid != pid) {
+ nxt_debug(task, "app \"%s\" real pid %d", init->name, nxt_pid);
+ nxt_debug(task, "app \"%s\" isolated pid: %d", init->name, pid);
+ }
- /* Clean inherited cached thread tid. */
- task->thread->tid = 0;
+ n = read(parentfd, &parent_status, sizeof(parent_status));
+ if (nxt_slow_path(n == -1 || n != sizeof(parent_status))) {
+ nxt_alert(task, "failed to read parent status");
+ return NXT_ERROR;
+ }
- process->pid = nxt_pid;
+ if (nxt_slow_path(close(parentfd) == -1)) {
+ nxt_alert(task, "failed to close reader pipe fd");
+ return NXT_ERROR;
+ }
- ptype = process->init->type;
+ if (nxt_slow_path(parent_status != NXT_OK)) {
+ return parent_status;
+ }
- nxt_port_reset_next_id();
+ ptype = init->type;
- nxt_event_engine_thread_adopt(task->thread->engine);
+ nxt_port_reset_next_id();
- /* Remove not ready processes */
- nxt_runtime_process_each(rt, p) {
+ nxt_event_engine_thread_adopt(task->thread->engine);
- if (nxt_proc_conn_matrix[ptype][nxt_process_type(p)] == 0) {
- nxt_debug(task, "remove not required process %PI", p->pid);
+ /* Remove not ready processes. */
+ nxt_runtime_process_each(rt, p) {
- nxt_process_close_ports(task, p);
+ if (nxt_proc_conn_matrix[ptype][nxt_process_type(p)] == 0) {
+ nxt_debug(task, "remove not required process %PI", p->pid);
- continue;
- }
+ nxt_process_close_ports(task, p);
- if (!p->ready) {
- nxt_debug(task, "remove not ready process %PI", p->pid);
+ continue;
+ }
- nxt_process_close_ports(task, p);
+ if (!p->ready) {
+ nxt_debug(task, "remove not ready process %PI", p->pid);
- continue;
- }
+ nxt_process_close_ports(task, p);
- nxt_port_mmaps_destroy(&p->incoming, 0);
- nxt_port_mmaps_destroy(&p->outgoing, 0);
+ continue;
+ }
- } nxt_runtime_process_loop;
+ nxt_port_mmaps_destroy(&p->incoming, 0);
+ nxt_port_mmaps_destroy(&p->outgoing, 0);
- nxt_runtime_process_add(task, process);
+ } nxt_runtime_process_loop;
- nxt_process_start(task, process);
+ nxt_runtime_process_add(task, process);
- process->ready = 1;
+ nxt_process_start(task, process);
- break;
+ process->ready = 1;
- default:
- /* A parent. */
- nxt_debug(task, "fork(\"%s\"): %PI", process->init->name, pid);
+ return NXT_OK;
+}
- process->pid = pid;
- nxt_runtime_process_add(task, process);
+nxt_pid_t
+nxt_process_create(nxt_task_t *task, nxt_process_t *process)
+{
+ int pipefd[2];
+ nxt_int_t ret;
+ nxt_pid_t pid;
+ nxt_process_init_t *init;
- break;
+ if (nxt_slow_path(pipe(pipefd) == -1)) {
+ nxt_alert(task, "failed to create process pipe for passing rpid");
+ return -1;
+ }
+
+ init = process->init;
+
+#if (NXT_HAVE_CLONE)
+ pid = nxt_clone(SIGCHLD|init->isolation.clone.flags);
+#else
+ pid = fork();
+#endif
+
+ if (nxt_slow_path(pid < 0)) {
+#if (NXT_HAVE_CLONE)
+ nxt_alert(task, "clone() failed while creating \"%s\" %E",
+ init->name, nxt_errno);
+#else
+ nxt_alert(task, "fork() failed while creating \"%s\" %E",
+ init->name, nxt_errno);
+#endif
+
+ return pid;
+ }
+
+ if (pid == 0) {
+ /* Child. */
+
+ if (nxt_slow_path(close(pipefd[1]) == -1)) {
+ nxt_alert(task, "failed to close writer pipe fd");
+ return NXT_ERROR;
+ }
+
+ ret = nxt_process_worker_setup(task, process, pipefd[0]);
+ if (nxt_slow_path(ret != NXT_OK)) {
+ exit(1);
+ }
+
+ /*
+ * Explicitly return 0 to notice the caller function this is the child.
+ * The caller must return to the event engine work queue loop.
+ */
+ return 0;
+ }
+
+ /* Parent. */
+
+ if (nxt_slow_path(close(pipefd[0]) != 0)) {
+ nxt_alert(task, "failed to close pipe: %E", nxt_errno);
+ }
+
+ /*
+ * At this point, the child process is blocked reading the
+ * pipe fd to get its real pid (rpid).
+ *
+ * If anything goes wrong now, we need to terminate the child
+ * process by sending a NXT_ERROR in the pipe.
+ */
+
+#if (NXT_HAVE_CLONE)
+ nxt_debug(task, "clone(\"%s\"): %PI", init->name, pid);
+#else
+ nxt_debug(task, "fork(\"%s\"): %PI", init->name, pid);
+#endif
+
+ if (nxt_slow_path(write(pipefd[1], &pid, sizeof(pid)) == -1)) {
+ nxt_alert(task, "failed to write real pid");
+ goto fail_cleanup;
+ }
+
+#if (NXT_HAVE_CLONE_NEWUSER)
+ if ((init->isolation.clone.flags & CLONE_NEWUSER) == CLONE_NEWUSER) {
+ ret = nxt_clone_proc_map(task, pid, &init->isolation.clone);
+ if (nxt_slow_path(ret != NXT_OK)) {
+ goto fail_cleanup;
+ }
+ }
+#endif
+
+ ret = NXT_OK;
+
+ if (nxt_slow_path(write(pipefd[1], &ret, sizeof(ret)) == -1)) {
+ nxt_alert(task, "failed to write status");
+ goto fail_cleanup;
}
+ process->pid = pid;
+
+ nxt_runtime_process_add(task, process);
+
return pid;
+
+fail_cleanup:
+
+ ret = NXT_ERROR;
+
+ if (nxt_slow_path(write(pipefd[1], &ret, sizeof(ret)) == -1)) {
+ nxt_alert(task, "failed to write status");
+ }
+
+ if (nxt_slow_path(close(pipefd[1]) != 0)) {
+ nxt_alert(task, "failed to close pipe: %E", nxt_errno);
+ }
+
+ waitpid(pid, NULL, 0);
+
+ return -1;
}
@@ -133,22 +272,17 @@ nxt_process_start(nxt_task_t *task, nxt_process_t *process)
nxt_process_title(task, "unit: %s", init->name);
thread = task->thread;
+ rt = thread->runtime;
nxt_random_init(&thread->random);
- if (init->user_cred != NULL) {
- /*
- * Changing user credentials requires either root privileges
- * or CAP_SETUID and CAP_SETGID capabilities on Linux.
- */
+ if (rt->capabilities.setid && init->user_cred != NULL) {
ret = nxt_user_cred_set(task, init->user_cred);
if (ret != NXT_OK) {
goto fail;
}
}
- rt = thread->runtime;
-
rt->type = init->type;
engine = thread->engine;
@@ -592,15 +726,8 @@ nxt_user_cred_set(nxt_task_t *task, nxt_user_cred_t *uc)
uc->user, (uint64_t) uc->uid, (uint64_t) uc->base_gid);
if (setgid(uc->base_gid) != 0) {
- if (nxt_errno == NXT_EPERM) {
- nxt_log(task, NXT_LOG_NOTICE, "setgid(%d) failed %E, ignored",
- uc->base_gid, nxt_errno);
- return NXT_OK;
-
- } else {
- nxt_alert(task, "setgid(%d) failed %E", uc->base_gid, nxt_errno);
- return NXT_ERROR;
- }
+ nxt_alert(task, "setgid(%d) failed %E", uc->base_gid, nxt_errno);
+ return NXT_ERROR;
}
if (uc->gids != NULL) {
diff --git a/src/nxt_process.h b/src/nxt_process.h
index c6e19f97..df9ca038 100644
--- a/src/nxt_process.h
+++ b/src/nxt_process.h
@@ -7,6 +7,8 @@
#ifndef _NXT_PROCESS_H_INCLUDED_
#define _NXT_PROCESS_H_INCLUDED_
+#include <nxt_conf.h>
+
typedef pid_t nxt_pid_t;
typedef uid_t nxt_uid_t;
@@ -21,26 +23,35 @@ typedef struct {
nxt_gid_t *gids;
} nxt_user_cred_t;
+typedef struct {
+ nxt_int_t flags;
+ nxt_conf_value_t *uidmap;
+ nxt_conf_value_t *gidmap;
+} nxt_process_clone_t;
+
typedef struct nxt_process_init_s nxt_process_init_t;
typedef nxt_int_t (*nxt_process_start_t)(nxt_task_t *task, void *data);
typedef nxt_int_t (*nxt_process_restart_t)(nxt_task_t *task, nxt_runtime_t *rt,
nxt_process_init_t *init);
-
struct nxt_process_init_s {
- nxt_process_start_t start;
- const char *name;
- nxt_user_cred_t *user_cred;
+ nxt_process_start_t start;
+ const char *name;
+ nxt_user_cred_t *user_cred;
+
+ nxt_port_handlers_t *port_handlers;
+ const nxt_sig_event_t *signals;
- nxt_port_handlers_t *port_handlers;
- const nxt_sig_event_t *signals;
+ nxt_process_type_t type;
- nxt_process_type_t type;
+ void *data;
+ uint32_t stream;
- void *data;
- uint32_t stream;
+ nxt_process_restart_t restart;
- nxt_process_restart_t restart;
+ union {
+ nxt_process_clone_t clone;
+ } isolation;
};
diff --git a/src/nxt_router.c b/src/nxt_router.c
index b87f588f..28781600 100644
--- a/src/nxt_router.c
+++ b/src/nxt_router.c
@@ -122,6 +122,8 @@ static void nxt_router_conf_send(nxt_task_t *task,
static nxt_int_t nxt_router_conf_create(nxt_task_t *task,
nxt_router_temp_conf_t *tmcf, u_char *start, u_char *end);
+static nxt_int_t nxt_router_conf_process_static(nxt_task_t *task,
+ nxt_router_conf_t *rtcf, nxt_conf_value_t *conf);
static nxt_app_t *nxt_router_app_find(nxt_queue_t *queue, nxt_str_t *name);
static void nxt_router_listen_socket_rpc_create(nxt_task_t *task,
nxt_router_temp_conf_t *tmcf, nxt_socket_conf_t *skcf);
@@ -247,7 +249,7 @@ static nxt_int_t nxt_router_http_request_done(nxt_task_t *task,
static void nxt_router_http_request_release(nxt_task_t *task, void *obj,
void *data);
-const nxt_http_request_state_t nxt_http_websocket;
+extern const nxt_http_request_state_t nxt_http_websocket;
static nxt_router_t *nxt_router;
@@ -1399,7 +1401,7 @@ nxt_router_conf_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf,
nxt_conf_value_t *conf, *http, *value, *websocket;
nxt_conf_value_t *applications, *application;
nxt_conf_value_t *listeners, *listener;
- nxt_conf_value_t *routes_conf;
+ nxt_conf_value_t *routes_conf, *static_conf;
nxt_socket_conf_t *skcf;
nxt_http_routes_t *routes;
nxt_event_engine_t *engine;
@@ -1419,6 +1421,7 @@ nxt_router_conf_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf,
#if (NXT_TLS)
static nxt_str_t certificate_path = nxt_string("/tls/certificate");
#endif
+ static nxt_str_t static_path = nxt_string("/settings/http/static");
static nxt_str_t websocket_path = nxt_string("/settings/http/websocket");
conf = nxt_conf_json_parse(tmcf->mem_pool, start, end, NULL);
@@ -1440,6 +1443,13 @@ nxt_router_conf_create(nxt_task_t *task, nxt_router_temp_conf_t *tmcf,
tmcf->router_conf->threads = nxt_ncpu;
}
+ static_conf = nxt_conf_get_path(conf, &static_path);
+
+ ret = nxt_router_conf_process_static(task, tmcf->router_conf, static_conf);
+ if (nxt_slow_path(ret != NXT_OK)) {
+ return NXT_ERROR;
+ }
+
router = tmcf->router_conf->router;
applications = nxt_conf_get_path(conf, &applications_path);
@@ -1788,6 +1798,87 @@ fail:
}
+static nxt_int_t
+nxt_router_conf_process_static(nxt_task_t *task, nxt_router_conf_t *rtcf,
+ nxt_conf_value_t *conf)
+{
+ uint32_t next, i;
+ nxt_mp_t *mp;
+ nxt_str_t *type, extension, str;
+ nxt_int_t ret;
+ nxt_uint_t exts;
+ nxt_conf_value_t *mtypes_conf, *ext_conf, *value;
+
+ static nxt_str_t mtypes_path = nxt_string("/mime_types");
+
+ mp = rtcf->mem_pool;
+
+ ret = nxt_http_static_mtypes_init(mp, &rtcf->mtypes_hash);
+ if (nxt_slow_path(ret != NXT_OK)) {
+ return NXT_ERROR;
+ }
+
+ if (conf == NULL) {
+ return NXT_OK;
+ }
+
+ mtypes_conf = nxt_conf_get_path(conf, &mtypes_path);
+
+ if (mtypes_conf != NULL) {
+ next = 0;
+
+ for ( ;; ) {
+ ext_conf = nxt_conf_next_object_member(mtypes_conf, &str, &next);
+
+ if (ext_conf == NULL) {
+ break;
+ }
+
+ type = nxt_str_dup(mp, NULL, &str);
+ if (nxt_slow_path(type == NULL)) {
+ return NXT_ERROR;
+ }
+
+ if (nxt_conf_type(ext_conf) == NXT_CONF_STRING) {
+ nxt_conf_get_string(ext_conf, &str);
+
+ if (nxt_slow_path(nxt_str_dup(mp, &extension, &str) == NULL)) {
+ return NXT_ERROR;
+ }
+
+ ret = nxt_http_static_mtypes_hash_add(mp, &rtcf->mtypes_hash,
+ &extension, type);
+ if (nxt_slow_path(ret != NXT_OK)) {
+ return NXT_ERROR;
+ }
+
+ continue;
+ }
+
+ exts = nxt_conf_array_elements_count(ext_conf);
+
+ for (i = 0; i < exts; i++) {
+ value = nxt_conf_get_array_element(ext_conf, i);
+
+ nxt_conf_get_string(value, &str);
+
+ if (nxt_slow_path(nxt_str_dup(mp, &extension, &str) == NULL)) {
+ return NXT_ERROR;
+ }
+
+ ret = nxt_http_static_mtypes_hash_add(mp, &rtcf->mtypes_hash,
+ &extension, type);
+ if (nxt_slow_path(ret != NXT_OK)) {
+ return NXT_ERROR;
+ }
+ }
+ }
+ }
+
+ return NXT_OK;
+}
+
+
static nxt_app_t *
nxt_router_app_find(nxt_queue_t *queue, nxt_str_t *name)
{
@@ -3531,7 +3622,7 @@ nxt_router_response_ready_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg,
nxt_buf_chain_add(&r->out, b);
}
- nxt_http_request_header_send(task, r);
+ nxt_http_request_header_send(task, r, nxt_http_request_send_body);
if (r->websocket_handshake
&& r->status == NXT_HTTP_SWITCHING_PROTOCOLS)
@@ -3575,11 +3666,6 @@ nxt_router_response_ready_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg,
} else {
r->state = &nxt_http_request_send_state;
}
-
- if (r->out) {
- nxt_work_queue_add(&task->thread->engine->fast_work_queue,
- nxt_http_request_send_body, task, r, NULL);
- }
}
return;
diff --git a/src/nxt_router.h b/src/nxt_router.h
index b55a4de3..ec18ff48 100644
--- a/src/nxt_router.h
+++ b/src/nxt_router.h
@@ -38,9 +38,13 @@ typedef struct {
typedef struct {
uint32_t count;
uint32_t threads;
+
+ nxt_mp_t *mem_pool;
+
nxt_router_t *router;
nxt_http_routes_t *routes;
- nxt_mp_t *mem_pool;
+
+ nxt_lvlhsh_t mtypes_hash;
nxt_router_access_log_t *access_log;
} nxt_router_conf_t;
diff --git a/src/nxt_runtime.c b/src/nxt_runtime.c
index 06478f72..de41ba4d 100644
--- a/src/nxt_runtime.c
+++ b/src/nxt_runtime.c
@@ -692,14 +692,26 @@ nxt_runtime_conf_init(nxt_task_t *task, nxt_runtime_t *rt)
rt->state = NXT_STATE;
rt->control = NXT_CONTROL_SOCK;
+ nxt_memzero(&rt->capabilities, sizeof(nxt_capabilities_t));
+
if (nxt_runtime_conf_read_cmd(task, rt) != NXT_OK) {
return NXT_ERROR;
}
- if (nxt_user_cred_get(task, &rt->user_cred, rt->group) != NXT_OK) {
+ if (nxt_capability_set(task, &rt->capabilities) != NXT_OK) {
return NXT_ERROR;
}
+ if (rt->capabilities.setid) {
+ if (nxt_user_cred_get(task, &rt->user_cred, rt->group) != NXT_OK) {
+ return NXT_ERROR;
+ }
+
+ } else {
+ nxt_log(task, NXT_LOG_WARN, "Unit is running unprivileged, then it "
+ "cannot use arbitrary user and group.");
+ }
+
/* An engine's parameters. */
interface = nxt_service_get(rt->services, "engine", rt->engine);
diff --git a/src/nxt_runtime.h b/src/nxt_runtime.h
index 496ae478..0791f8e7 100644
--- a/src/nxt_runtime.h
+++ b/src/nxt_runtime.h
@@ -59,6 +59,7 @@ struct nxt_runtime_s {
uint32_t engine_connections;
uint32_t auxiliary_threads;
nxt_user_cred_t user_cred;
+ nxt_capabilities_t capabilities;
const char *group;
const char *pid;
const char *log;
diff --git a/src/nxt_sprintf.c b/src/nxt_sprintf.c
index 240f47ef..9557b327 100644
--- a/src/nxt_sprintf.c
+++ b/src/nxt_sprintf.c
@@ -12,8 +12,8 @@
/*
* Supported formats:
*
- * %[0][width][x][X]O nxt_off_t
- * %[0][width]T nxt_time_t
+ * %[0][width][x|X]O nxt_off_t
+ * %[0][width][x|X]T nxt_time_t
* %[0][width][u][x|X]z ssize_t/size_t
* %[0][width][u][x|X]d int/u_int
* %[0][width][u][x|X]l long
diff --git a/src/nxt_string.c b/src/nxt_string.c
index 7d8c1ce3..b89e9555 100644
--- a/src/nxt_string.c
+++ b/src/nxt_string.c
@@ -451,3 +451,125 @@ nxt_strvers_match(u_char *version, u_char *prefix, size_t length)
return 0;
}
+
+
+u_char *
+nxt_decode_uri(u_char *dst, u_char *src, size_t length)
+{
+ u_char *end, ch;
+ uint8_t d0, d1;
+
+ static const uint8_t hex[256]
+ nxt_aligned(32) =
+ {
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 16, 16, 16, 16, 16,
+ 16, 10, 11, 12, 13, 14, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 10, 11, 12, 13, 14, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ };
+
+ nxt_prefetch(&hex['0']);
+
+ end = src + length;
+
+ while (src < end) {
+ ch = *src++;
+
+ if (ch == '%') {
+ if (nxt_slow_path(end - src < 2)) {
+ return NULL;
+ }
+
+ d0 = hex[*src++];
+ d1 = hex[*src++];
+
+ if (nxt_slow_path((d0 | d1) >= 16)) {
+ return NULL;
+ }
+
+ ch = (d0 << 4) + d1;
+ }
+
+ *dst++ = ch;
+ }
+
+ return dst;
+}
+
+
+uintptr_t
+nxt_encode_uri(u_char *dst, u_char *src, size_t length)
+{
+ u_char *end;
+ nxt_uint_t n;
+
+ static const u_char hex[16] = "0123456789ABCDEF";
+
+ /* " ", "#", "%", "?", %00-%1F, %7F-%FF */
+
+ static const uint32_t escape[] = {
+ 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */
+
+ /* ?>=< ;:98 7654 3210 /.-, +*)( '&%$ #"! */
+ 0x80000029, /* 1000 0000 0000 0000 0000 0000 0010 1001 */
+
+ /* _^]\ [ZYX WVUT SRQP ONML KJIH GFED CBA@ */
+ 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */
+
+ /* ~}| {zyx wvut srqp onml kjih gfed cba` */
+ 0x80000000, /* 1000 0000 0000 0000 0000 0000 0000 0000 */
+
+ 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */
+ 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */
+ 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */
+ 0xffffffff /* 1111 1111 1111 1111 1111 1111 1111 1111 */
+ };
+
+ end = src + length;
+
+ if (dst == NULL) {
+
+ /* Find the number of the characters to be escaped. */
+
+ n = 0;
+
+ while (src < end) {
+
+ if (escape[*src >> 5] & (1U << (*src & 0x1f))) {
+ n++;
+ }
+
+ src++;
+ }
+
+ return (uintptr_t) n;
+ }
+
+ while (src < end) {
+
+ if (escape[*src >> 5] & (1U << (*src & 0x1f))) {
+ *dst++ = '%';
+ *dst++ = hex[*src >> 4];
+ *dst++ = hex[*src & 0xf];
+
+ } else {
+ *dst++ = *src;
+ }
+
+ src++;
+ }
+
+ return (uintptr_t) dst;
+}
diff --git a/src/nxt_string.h b/src/nxt_string.h
index 22a63a17..8d7b3b73 100644
--- a/src/nxt_string.h
+++ b/src/nxt_string.h
@@ -168,5 +168,8 @@ NXT_EXPORT nxt_int_t nxt_strverscmp(const u_char *s1, const u_char *s2);
NXT_EXPORT nxt_bool_t nxt_strvers_match(u_char *version, u_char *prefix,
size_t length);
+NXT_EXPORT u_char *nxt_decode_uri(u_char *dst, u_char *src, size_t length);
+NXT_EXPORT uintptr_t nxt_encode_uri(u_char *dst, u_char *src, size_t length);
+
#endif /* _NXT_STRING_H_INCLUDED_ */
diff --git a/src/nxt_unit.c b/src/nxt_unit.c
index 28a0de20..9ccd1fd9 100644
--- a/src/nxt_unit.c
+++ b/src/nxt_unit.c
@@ -31,7 +31,7 @@ typedef struct nxt_unit_request_info_impl_s nxt_unit_request_info_impl_t;
typedef struct nxt_unit_websocket_frame_impl_s nxt_unit_websocket_frame_impl_t;
static nxt_unit_impl_t *nxt_unit_create(nxt_unit_init_t *init);
-static void nxt_unit_ctx_init(nxt_unit_impl_t *lib,
+static int nxt_unit_ctx_init(nxt_unit_impl_t *lib,
nxt_unit_ctx_impl_t *ctx_impl, void *data);
nxt_inline void nxt_unit_mmap_buf_insert(nxt_unit_mmap_buf_t **head,
nxt_unit_mmap_buf_t *mmap_buf);
@@ -204,6 +204,8 @@ struct nxt_unit_websocket_frame_impl_s {
struct nxt_unit_ctx_impl_s {
nxt_unit_ctx_t ctx;
+ pthread_mutex_t mutex;
+
nxt_unit_port_id_t read_port_id;
int read_port_fd;
@@ -331,6 +333,7 @@ nxt_unit_init(nxt_unit_init_t *init)
}
}
+ lib->pid = read_port.id.pid;
ctx = &lib->main_ctx.ctx;
rc = lib->callbacks.add_port(ctx, &ready_port);
@@ -396,13 +399,15 @@ nxt_unit_create(nxt_unit_init_t *init)
lib->processes.slot = NULL;
lib->ports.slot = NULL;
- lib->pid = getpid();
lib->log_fd = STDERR_FILENO;
lib->online = 1;
nxt_queue_init(&lib->contexts);
- nxt_unit_ctx_init(lib, &lib->main_ctx, init->ctx_data);
+ rc = nxt_unit_ctx_init(lib, &lib->main_ctx, init->ctx_data);
+ if (nxt_slow_path(rc != NXT_UNIT_OK)) {
+ goto fail;
+ }
cb = &lib->callbacks;
@@ -446,15 +451,24 @@ fail:
}
-static void
+static int
nxt_unit_ctx_init(nxt_unit_impl_t *lib, nxt_unit_ctx_impl_t *ctx_impl,
void *data)
{
+ int rc;
+
ctx_impl->ctx.data = data;
ctx_impl->ctx.unit = &lib->unit;
nxt_queue_insert_tail(&lib->contexts, &ctx_impl->link);
+ rc = pthread_mutex_init(&ctx_impl->mutex, NULL);
+ if (nxt_slow_path(rc != 0)) {
+ nxt_unit_alert(NULL, "failed to initialize mutex (%d)", rc);
+
+ return NXT_UNIT_ERROR;
+ }
+
nxt_queue_init(&ctx_impl->free_req);
nxt_queue_init(&ctx_impl->free_ws);
nxt_queue_init(&ctx_impl->active_req);
@@ -470,6 +484,8 @@ nxt_unit_ctx_init(nxt_unit_impl_t *lib, nxt_unit_ctx_impl_t *ctx_impl,
ctx_impl->read_port_fd = -1;
ctx_impl->requests.slot = 0;
+
+ return NXT_UNIT_OK;
}
@@ -962,6 +978,11 @@ nxt_unit_process_websocket(nxt_unit_ctx_t *ctx, nxt_unit_recv_msg_t *recv_msg)
} else {
b = nxt_unit_mmap_buf_get(ctx);
if (nxt_slow_path(b == NULL)) {
+ nxt_unit_alert(ctx, "#%"PRIu32": failed to allocate buf",
+ req_impl->stream);
+
+ nxt_unit_websocket_frame_release(&ws_impl->ws);
+
return NXT_UNIT_ERROR;
}
@@ -1029,18 +1050,22 @@ nxt_unit_request_info_get(nxt_unit_ctx_t *ctx)
lib = nxt_container_of(ctx->unit, nxt_unit_impl_t, unit);
+ pthread_mutex_lock(&ctx_impl->mutex);
+
if (nxt_queue_is_empty(&ctx_impl->free_req)) {
+ pthread_mutex_unlock(&ctx_impl->mutex);
+
req_impl = malloc(sizeof(nxt_unit_request_info_impl_t)
+ lib->request_data_size);
if (nxt_slow_path(req_impl == NULL)) {
- nxt_unit_warn(ctx, "request info allocation failed");
-
return NULL;
}
req_impl->req.unit = ctx->unit;
req_impl->req.ctx = ctx;
+ pthread_mutex_lock(&ctx_impl->mutex);
+
} else {
lnk = nxt_queue_first(&ctx_impl->free_req);
nxt_queue_remove(lnk);
@@ -1050,6 +1075,8 @@ nxt_unit_request_info_get(nxt_unit_ctx_t *ctx)
nxt_queue_insert_tail(&ctx_impl->active_req, &req_impl->link);
+ pthread_mutex_unlock(&ctx_impl->mutex);
+
req_impl->req.data = lib->request_data_size ? req_impl->extra_data : NULL;
return req_impl;
@@ -1068,12 +1095,6 @@ nxt_unit_request_info_release(nxt_unit_request_info_t *req)
req->response = NULL;
req->response_buf = NULL;
- if (req_impl->process != NULL) {
- nxt_unit_process_use(req->ctx, req_impl->process, -1);
-
- req_impl->process = NULL;
- }
-
if (req_impl->websocket) {
nxt_unit_request_hash_find(&ctx_impl->requests, req_impl->stream, 1);
@@ -1088,10 +1109,24 @@ nxt_unit_request_info_release(nxt_unit_request_info_t *req)
nxt_unit_mmap_buf_free(req_impl->incoming_buf);
}
+ /*
+ * Process release should go after buffers release to guarantee mmap
+ * existence.
+ */
+ if (req_impl->process != NULL) {
+ nxt_unit_process_use(req->ctx, req_impl->process, -1);
+
+ req_impl->process = NULL;
+ }
+
+ pthread_mutex_lock(&ctx_impl->mutex);
+
nxt_queue_remove(&req_impl->link);
nxt_queue_insert_tail(&ctx_impl->free_req, &req_impl->link);
+ pthread_mutex_unlock(&ctx_impl->mutex);
+
req_impl->state = NXT_UNIT_RS_RELEASED;
}
@@ -1120,11 +1155,13 @@ nxt_unit_websocket_frame_get(nxt_unit_ctx_t *ctx)
ctx_impl = nxt_container_of(ctx, nxt_unit_ctx_impl_t, ctx);
+ pthread_mutex_lock(&ctx_impl->mutex);
+
if (nxt_queue_is_empty(&ctx_impl->free_ws)) {
+ pthread_mutex_unlock(&ctx_impl->mutex);
+
ws_impl = malloc(sizeof(nxt_unit_websocket_frame_impl_t));
if (nxt_slow_path(ws_impl == NULL)) {
- nxt_unit_warn(ctx, "websocket frame allocation failed");
-
return NULL;
}
@@ -1132,6 +1169,8 @@ nxt_unit_websocket_frame_get(nxt_unit_ctx_t *ctx)
lnk = nxt_queue_first(&ctx_impl->free_ws);
nxt_queue_remove(lnk);
+ pthread_mutex_unlock(&ctx_impl->mutex);
+
ws_impl = nxt_container_of(lnk, nxt_unit_websocket_frame_impl_t, link);
}
@@ -1160,7 +1199,11 @@ nxt_unit_websocket_frame_release(nxt_unit_websocket_frame_t *ws)
ws_impl->retain_buf = NULL;
}
+ pthread_mutex_lock(&ws_impl->ctx_impl->mutex);
+
nxt_queue_insert_tail(&ws_impl->ctx_impl->free_ws, &ws_impl->link);
+
+ pthread_mutex_unlock(&ws_impl->ctx_impl->mutex);
}
@@ -1635,6 +1678,8 @@ nxt_unit_response_buf_alloc(nxt_unit_request_info_t *req, uint32_t size)
mmap_buf = nxt_unit_mmap_buf_get(req->ctx);
if (nxt_slow_path(mmap_buf == NULL)) {
+ nxt_unit_req_alert(req, "response_buf_alloc: failed to allocate buf");
+
return NULL;
}
@@ -1688,16 +1733,22 @@ nxt_unit_mmap_buf_get(nxt_unit_ctx_t *ctx)
ctx_impl = nxt_container_of(ctx, nxt_unit_ctx_impl_t, ctx);
+ pthread_mutex_lock(&ctx_impl->mutex);
+
if (ctx_impl->free_buf == NULL) {
+ pthread_mutex_unlock(&ctx_impl->mutex);
+
mmap_buf = malloc(sizeof(nxt_unit_mmap_buf_t));
if (nxt_slow_path(mmap_buf == NULL)) {
- nxt_unit_warn(ctx, "failed to allocate buf");
+ return NULL;
}
} else {
mmap_buf = ctx_impl->free_buf;
nxt_unit_mmap_buf_remove(mmap_buf);
+
+ pthread_mutex_unlock(&ctx_impl->mutex);
}
mmap_buf->ctx_impl = ctx_impl;
@@ -1711,7 +1762,11 @@ nxt_unit_mmap_buf_release(nxt_unit_mmap_buf_t *mmap_buf)
{
nxt_unit_mmap_buf_remove(mmap_buf);
+ pthread_mutex_lock(&mmap_buf->ctx_impl->mutex);
+
nxt_unit_mmap_buf_insert(&mmap_buf->ctx_impl->free_buf, mmap_buf);
+
+ pthread_mutex_unlock(&mmap_buf->ctx_impl->mutex);
}
@@ -3298,7 +3353,14 @@ nxt_unit_ctx_alloc(nxt_unit_ctx_t *ctx, void *data)
close(fd);
- nxt_unit_ctx_init(lib, new_ctx, data);
+ rc = nxt_unit_ctx_init(lib, new_ctx, data);
+ if (nxt_slow_path(rc != NXT_UNIT_OK)) {
+ lib->callbacks.remove_port(ctx, &new_port_id);
+
+ free(new_ctx);
+
+ return NULL;
+ }
new_ctx->read_port_id = new_port_id;
@@ -3350,6 +3412,8 @@ nxt_unit_ctx_free(nxt_unit_ctx_t *ctx)
} nxt_queue_loop;
+ pthread_mutex_destroy(&ctx_impl->mutex);
+
nxt_queue_remove(&ctx_impl->link);
if (ctx_impl != &lib->main_ctx) {
diff --git a/src/test/nxt_http_parse_test.c b/src/test/nxt_http_parse_test.c
index 572e91b2..5498cb1f 100644
--- a/src/test/nxt_http_parse_test.c
+++ b/src/test/nxt_http_parse_test.c
@@ -21,8 +21,6 @@ typedef struct {
unsigned quoted_target:1;
/* target with " " */
unsigned space_in_target:1;
- /* target with "+" */
- unsigned plus_in_target:1;
} nxt_http_parse_test_request_line_t;
@@ -70,7 +68,7 @@ static nxt_http_parse_test_case_t nxt_http_test_cases[] = {
nxt_null_string,
nxt_null_string,
"HTTP/1.0",
- 0, 0, 0, 0
+ 0, 0, 0
}}
},
{
@@ -80,10 +78,10 @@ static nxt_http_parse_test_case_t nxt_http_test_cases[] = {
{ .request_line = {
nxt_string("XXX-METHOD"),
nxt_string("/d.ir/fi+le.ext?key=val"),
- nxt_string("ext?key=val"),
+ nxt_string("ext"),
nxt_string("key=val"),
"HTTP/1.2",
- 0, 0, 0, 1
+ 0, 0, 0
}}
},
{
@@ -96,7 +94,7 @@ static nxt_http_parse_test_case_t nxt_http_test_cases[] = {
nxt_null_string,
nxt_string(""),
"HTTP/1.0",
- 0, 0, 0, 0
+ 0, 0, 0
}}
},
{
@@ -139,7 +137,7 @@ static nxt_http_parse_test_case_t nxt_http_test_cases[] = {
nxt_null_string,
nxt_null_string,
"HTTP/1.0",
- 1, 0, 0, 0
+ 1, 0, 0
}}
},
{
@@ -152,7 +150,7 @@ static nxt_http_parse_test_case_t nxt_http_test_cases[] = {
nxt_null_string,
nxt_null_string,
"HTTP/1.0",
- 1, 0, 0, 0
+ 1, 0, 0
}}
},
{
@@ -163,9 +161,9 @@ static nxt_http_parse_test_case_t nxt_http_test_cases[] = {
nxt_string("GET"),
nxt_string("/?#"),
nxt_null_string,
- nxt_string("#"),
+ nxt_string(""),
"HTTP/1.0",
- 1, 0, 0, 0
+ 1, 0, 0
}}
},
{
@@ -178,7 +176,7 @@ static nxt_http_parse_test_case_t nxt_http_test_cases[] = {
nxt_null_string,
nxt_null_string,
"HTTP/1.0",
- 1, 0, 0, 0
+ 1, 0, 0
}}
},
{
@@ -191,7 +189,7 @@ static nxt_http_parse_test_case_t nxt_http_test_cases[] = {
nxt_null_string,
nxt_null_string,
"HTTP/1.0",
- 0, 1, 0, 0
+ 0, 1, 0
}}
},
{
@@ -204,7 +202,20 @@ static nxt_http_parse_test_case_t nxt_http_test_cases[] = {
nxt_null_string,
nxt_null_string,
"HTTP/1.0",
- 0, 0, 1, 0
+ 0, 0, 1
+ }}
+ },
+ {
+ nxt_string("GET /na %20me.ext?args HTTP/1.0\r\n\r\n"),
+ NXT_DONE,
+ &nxt_http_parse_test_request_line,
+ { .request_line = {
+ nxt_string("GET"),
+ nxt_string("/na %20me.ext?args"),
+ nxt_string("ext"),
+ nxt_string("args"),
+ "HTTP/1.0",
+ 0, 1, 1
}}
},
{
@@ -214,10 +225,10 @@ static nxt_http_parse_test_case_t nxt_http_test_cases[] = {
{ .request_line = {
nxt_string("GET"),
nxt_string("/ HTTP/1.0"),
- nxt_null_string,
+ nxt_string("0"),
nxt_null_string,
"HTTP/1.1",
- 0, 0, 1, 0
+ 0, 0, 1
}}
},
{
@@ -729,31 +740,23 @@ nxt_http_parse_test_request_line(nxt_http_request_parse_t *rp,
return NXT_ERROR;
}
- str.length = (rp->exten_start != NULL) ? rp->target_end - rp->exten_start
- : 0;
- str.start = rp->exten_start;
-
- if (str.start != test->exten.start
- && !nxt_strstr_eq(&str, &test->exten))
+ if (rp->exten.start != test->exten.start
+ && !nxt_strstr_eq(&rp->exten, &test->exten))
{
nxt_log_alert(log, "http parse test case failed:\n"
" - request:\n\"%V\"\n"
" - exten: \"%V\" (expected: \"%V\")",
- request, &str, &test->exten);
+ request, &rp->exten, &test->exten);
return NXT_ERROR;
}
- str.length = (rp->args_start != NULL) ? rp->target_end - rp->args_start
- : 0;
- str.start = rp->args_start;
-
- if (str.start != test->args.start
- && !nxt_strstr_eq(&str, &test->args))
+ if (rp->args.start != test->args.start
+ && !nxt_strstr_eq(&rp->args, &test->args))
{
nxt_log_alert(log, "http parse test case failed:\n"
" - request:\n\"%V\"\n"
" - args: \"%V\" (expected: \"%V\")",
- request, &str, &test->args);
+ request, &rp->args, &test->args);
return NXT_ERROR;
}
@@ -790,14 +793,6 @@ nxt_http_parse_test_request_line(nxt_http_request_parse_t *rp,
return NXT_ERROR;
}
- if (rp->plus_in_target != test->plus_in_target) {
- nxt_log_alert(log, "http parse test case failed:\n"
- " - request:\n\"%V\"\n"
- " - plus_in_target: %d (expected: %d)",
- request, rp->plus_in_target, test->plus_in_target);
- return NXT_ERROR;
- }
-
return NXT_OK;
}
diff --git a/test/go/ns_inspect/app.go b/test/go/ns_inspect/app.go
new file mode 100644
index 00000000..ebecbb00
--- /dev/null
+++ b/test/go/ns_inspect/app.go
@@ -0,0 +1,79 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "nginx/unit"
+ "os"
+ "strconv"
+)
+
+type (
+ NS struct {
+ USER uint64
+ PID uint64
+ IPC uint64
+ CGROUP uint64
+ UTS uint64
+ MNT uint64
+ NET uint64
+ }
+
+ Output struct {
+ PID int
+ UID int
+ GID int
+ NS NS
+ }
+)
+
+func abortonerr(err error) {
+ if err != nil {
+ panic(err)
+ }
+}
+
+// returns: [nstype]:[4026531835]
+func getns(nstype string) uint64 {
+ str, err := os.Readlink(fmt.Sprintf("/proc/self/ns/%s", nstype))
+ if err != nil {
+ return 0
+ }
+
+ str = str[len(nstype)+2:]
+ str = str[:len(str)-1]
+ val, err := strconv.ParseUint(str, 10, 64)
+ abortonerr(err)
+ return val
+}
+
+func handler(w http.ResponseWriter, r *http.Request) {
+ pid := os.Getpid()
+ out := &Output{
+ PID: pid,
+ UID: os.Getuid(),
+ GID: os.Getgid(),
+ NS: NS{
+ PID: getns("pid"),
+ USER: getns("user"),
+ MNT: getns("mnt"),
+ IPC: getns("ipc"),
+ UTS: getns("uts"),
+ NET: getns("net"),
+ CGROUP: getns("cgroup"),
+ },
+ }
+ data, err := json.Marshal(out)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ w.Write(data)
+}
+
+func main() {
+ http.HandleFunc("/", handler)
+ unit.ListenAndServe(":7080", nil)
+}
diff --git a/test/java/websockets_mirror/app.java b/test/java/websockets_mirror/app.java
new file mode 100644
index 00000000..ada60231
--- /dev/null
+++ b/test/java/websockets_mirror/app.java
@@ -0,0 +1,57 @@
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.PongMessage;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+@ServerEndpoint("/")
+public class app {
+
+ @OnOpen
+ public void onOpen(Session session) {
+ session.setMaxTextMessageBufferSize(8388608);
+ }
+
+ @OnMessage
+ public void echoTextMessage(Session session, String msg) {
+ try {
+ if (session.isOpen()) {
+ session.getBasicRemote().sendText(msg, true);
+ }
+ } catch (IOException e) {
+ try {
+ session.close();
+ } catch (IOException e1) {
+ // Ignore
+ }
+ }
+ }
+
+ @OnMessage
+ public void echoBinaryMessage(Session session, ByteBuffer bb) {
+ try {
+ if (session.isOpen()) {
+ session.getBasicRemote().sendBinary(bb, true);
+ }
+ } catch (IOException e) {
+ try {
+ session.close();
+ } catch (IOException e1) {
+ // Ignore
+ }
+ }
+ }
+
+ /**
+ * Process a received pong. This is a NO-OP.
+ *
+ * @param pm Ignored.
+ */
+ @OnMessage
+ public void echoPongMessage(PongMessage pm) {
+ // NO-OP
+ }
+}
diff --git a/test/test_access_log.py b/test/test_access_log.py
index fbcc131f..8dc87524 100644
--- a/test/test_access_log.py
+++ b/test/test_access_log.py
@@ -7,7 +7,7 @@ from unit.applications.lang.python import TestApplicationPython
class TestAccessLog(TestApplicationPython):
- prerequisites = ['python']
+ prerequisites = {'modules': ['python']}
def load(self, script):
super().load(script)
diff --git a/test/test_configuration.py b/test/test_configuration.py
index 6e59c0a7..69647858 100644
--- a/test/test_configuration.py
+++ b/test/test_configuration.py
@@ -3,7 +3,7 @@ from unit.control import TestControl
class TestConfiguration(TestControl):
- prerequisites = ['python']
+ prerequisites = {'modules': ['python']}
def test_json_empty(self):
self.assertIn('error', self.conf(''), 'empty')
diff --git a/test/test_go_application.py b/test/test_go_application.py
index 488bfdd5..42429be7 100644
--- a/test/test_go_application.py
+++ b/test/test_go_application.py
@@ -2,7 +2,7 @@ from unit.applications.lang.go import TestApplicationGo
class TestGoApplication(TestApplicationGo):
- prerequisites = ['go']
+ prerequisites = {'modules': ['go']}
def test_go_application_variables(self):
self.load('variables')
diff --git a/test/test_go_isolation.py b/test/test_go_isolation.py
new file mode 100644
index 00000000..780c2b03
--- /dev/null
+++ b/test/test_go_isolation.py
@@ -0,0 +1,135 @@
+import os
+import json
+import unittest
+from unit.applications.lang.go import TestApplicationGo
+from unit.feature.isolation import TestFeatureIsolation
+
+
+class TestGoIsolation(TestApplicationGo):
+ prerequisites = {'modules': ['go'], 'features': ['isolation']}
+
+ isolation = TestFeatureIsolation()
+
+ @classmethod
+ def setUpClass(cls, complete_check=True):
+ unit = super().setUpClass(complete_check=False)
+
+ TestFeatureIsolation().check(cls.available, unit.testdir)
+
+ return unit if not complete_check else unit.complete()
+
+ def isolation_key(self, key):
+ return key in self.available['features']['isolation'].keys()
+
+ def conf_isolation(self, isolation):
+ self.assertIn(
+ 'success',
+ self.conf(isolation, 'applications/ns_inspect/isolation'),
+ 'configure isolation',
+ )
+
+ def test_isolation_values(self):
+ self.load('ns_inspect')
+
+ obj = self.isolation.parsejson(self.get()['body'])
+
+ for ns, ns_value in self.available['features']['isolation'].items():
+ if ns.upper() in obj['NS']:
+ self.assertEqual(
+ obj['NS'][ns.upper()], ns_value, '%s match' % ns
+ )
+
+ def test_isolation_user(self):
+ if not self.isolation_key('unprivileged_userns_clone'):
+ print('unprivileged clone is not available')
+ raise unittest.SkipTest()
+
+ self.load('ns_inspect')
+ obj = self.isolation.parsejson(self.get()['body'])
+
+ self.assertTrue(obj['UID'] != 0, 'uid not zero')
+ self.assertTrue(obj['GID'] != 0, 'gid not zero')
+ self.assertEqual(obj['UID'], os.getuid(), 'uid match')
+ self.assertEqual(obj['GID'], os.getgid(), 'gid match')
+
+ self.conf_isolation({"namespaces": {"credential": True}})
+
+ obj = self.isolation.parsejson(self.get()['body'])
+
+ # default uid and gid maps current user to nobody
+ self.assertEqual(obj['UID'], 65534, 'uid nobody')
+ self.assertEqual(obj['GID'], 65534, 'gid nobody')
+
+ self.conf_isolation(
+ {
+ "namespaces": {"credential": True},
+ "uidmap": [
+ {"container": 1000, "host": os.geteuid(), "size": 1}
+ ],
+ "gidmap": [
+ {"container": 1000, "host": os.getegid(), "size": 1}
+ ],
+ }
+ )
+
+ obj = self.isolation.parsejson(self.get()['body'])
+
+ # default uid and gid maps current user to root
+ self.assertEqual(obj['UID'], 1000, 'uid root')
+ self.assertEqual(obj['GID'], 1000, 'gid root')
+
+ def test_isolation_mnt(self):
+ if not self.isolation_key('mnt'):
+ print('mnt namespace is not supported')
+ raise unittest.SkipTest()
+
+ if not self.isolation_key('unprivileged_userns_clone'):
+ print('unprivileged clone is not available')
+ raise unittest.SkipTest()
+
+ self.load('ns_inspect')
+ self.conf_isolation(
+ {"namespaces": {"mount": True, "credential": True}}
+ )
+
+ obj = self.isolation.parsejson(self.get()['body'])
+
+ # all but user and mnt
+ allns = list(self.available['features']['isolation'].keys())
+ allns.remove('user')
+ allns.remove('mnt')
+
+ for ns in allns:
+ if ns.upper() in obj['NS']:
+ self.assertEqual(
+ obj['NS'][ns.upper()],
+ self.available['features']['isolation'][ns],
+ '%s match' % ns,
+ )
+
+ self.assertNotEqual(
+ obj['NS']['MNT'], self.isolation.getns('mnt'), 'mnt set'
+ )
+ self.assertNotEqual(
+ obj['NS']['USER'], self.isolation.getns('user'), 'user set'
+ )
+
+ def test_isolation_pid(self):
+ if not self.isolation_key('pid'):
+ print('pid namespace is not supported')
+ raise unittest.SkipTest()
+
+ if not self.isolation_key('unprivileged_userns_clone'):
+ print('unprivileged clone is not available')
+ raise unittest.SkipTest()
+
+ self.load('ns_inspect')
+ self.conf_isolation({"namespaces": {"pid": True, "credential": True}})
+
+ obj = self.isolation.parsejson(self.get()['body'])
+
+ self.assertEqual(obj['PID'], 1, 'pid of container is 1')
+
+
+if __name__ == '__main__':
+ TestGoIsolation.main()
diff --git a/test/test_http_header.py b/test/test_http_header.py
index 603f6f0f..b773bd68 100644
--- a/test/test_http_header.py
+++ b/test/test_http_header.py
@@ -3,7 +3,7 @@ from unit.applications.lang.python import TestApplicationPython
class TestHTTPHeader(TestApplicationPython):
- prerequisites = ['python']
+ prerequisites = {'modules': ['python']}
def test_http_header_value_leading_sp(self):
self.load('custom_header')
diff --git a/test/test_java_application.py b/test/test_java_application.py
index 526be565..2e937718 100644
--- a/test/test_java_application.py
+++ b/test/test_java_application.py
@@ -4,7 +4,7 @@ from unit.applications.lang.java import TestApplicationJava
class TestJavaApplication(TestApplicationJava):
- prerequisites = ['java']
+ prerequisites = {'modules': ['java']}
def test_java_conf_error(self):
self.skip_alerts.extend(
diff --git a/test/test_java_websockets.py b/test/test_java_websockets.py
new file mode 100644
index 00000000..3f2c0a8a
--- /dev/null
+++ b/test/test_java_websockets.py
@@ -0,0 +1,1469 @@
+import time
+import struct
+import unittest
+from unit.applications.lang.java import TestApplicationJava
+from unit.applications.websockets import TestApplicationWebsocket
+
+
+class TestJavaWebsockets(TestApplicationJava):
+ prerequisites = {'modules': ['java']}
+
+ ws = TestApplicationWebsocket(True)
+
+ def setUp(self):
+ super().setUp()
+
+ self.assertIn(
+ 'success',
+ self.conf(
+ {'http': {'websocket': {'keepalive_interval': 0}}}, 'settings'
+ ),
+ 'clear keepalive_interval',
+ )
+
+ self.skip_alerts.extend(
+ [r'last message send failed', r'socket close\(\d+\) failed']
+ )
+
+ def close_connection(self, sock):
+ self.assertEqual(self.recvall(sock, read_timeout=1), b'', 'empty sock')
+
+ self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close())
+
+ self.check_close(sock)
+
+ def check_close(self, sock, code=1000, no_close=False):
+ frame = self.ws.frame_read(sock)
+
+ self.assertEqual(frame['fin'], True, 'close fin')
+ self.assertEqual(frame['opcode'], self.ws.OP_CLOSE, 'close opcode')
+ self.assertEqual(frame['code'], code, 'close code')
+
+ if not no_close:
+ sock.close()
+
+ def check_frame(self, frame, fin, opcode, payload, decode=True):
+ if opcode == self.ws.OP_BINARY or not decode:
+ data = frame['data']
+ else:
+ data = frame['data'].decode('utf-8')
+
+ self.assertEqual(frame['fin'], fin, 'fin')
+ self.assertEqual(frame['opcode'], opcode, 'opcode')
+ self.assertEqual(data, payload, 'payload')
+
+ def test_java_websockets_handshake(self):
+ self.load('websockets_mirror')
+
+ resp, sock, key = self.ws.upgrade()
+ sock.close()
+
+ self.assertEqual(resp['status'], 101, 'status')
+ self.assertEqual(resp['headers']['Upgrade'], 'websocket', 'upgrade')
+ self.assertEqual(
+ resp['headers']['Connection'], 'Upgrade', 'connection'
+ )
+ self.assertEqual(
+ resp['headers']['Sec-WebSocket-Accept'], self.ws.accept(key), 'key'
+ )
+
+ def test_java_websockets_mirror(self):
+ self.load('websockets_mirror')
+
+ message = 'blah'
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, message)
+ frame = self.ws.frame_read(sock)
+
+ self.assertEqual(message, frame['data'].decode('utf-8'), 'mirror')
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, message)
+ frame = self.ws.frame_read(sock)
+
+ self.assertEqual(message, frame['data'].decode('utf-8'), 'mirror 2')
+
+ sock.close()
+
+ def test_java_websockets_no_mask(self):
+ self.load('websockets_mirror')
+
+ message = 'blah'
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, message, mask=False)
+
+ frame = self.ws.frame_read(sock)
+
+ self.assertEqual(frame['opcode'], self.ws.OP_CLOSE, 'no mask opcode')
+ self.assertEqual(frame['code'], 1002, 'no mask close code')
+
+ sock.close()
+
+ def test_java_websockets_fragmentation(self):
+ self.load('websockets_mirror')
+
+ message = 'blah'
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, message, fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CONT, ' ', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CONT, message)
+
+ frame = self.ws.frame_read(sock)
+
+ self.assertEqual(
+ message + ' ' + message,
+ frame['data'].decode('utf-8'),
+ 'mirror framing',
+ )
+
+ sock.close()
+
+ def test_java_websockets_frame_fragmentation_invalid(self):
+ self.load('websockets_mirror')
+
+ message = 'blah'
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_PING, message, fin=False)
+
+ frame = self.ws.frame_read(sock)
+
+ frame.pop('data')
+ self.assertDictEqual(
+ frame,
+ {
+ 'fin': True,
+ 'rsv1': False,
+ 'rsv2': False,
+ 'rsv3': False,
+ 'opcode': self.ws.OP_CLOSE,
+ 'mask': 0,
+ 'code': 1002,
+ 'reason': 'Fragmented control frame',
+ },
+ 'close frame',
+ )
+
+ sock.close()
+
+ def test_java_websockets_two_clients(self):
+ self.load('websockets_mirror')
+
+ message1 = 'blah1'
+ message2 = 'blah2'
+
+ _, sock1, _ = self.ws.upgrade()
+ _, sock2, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock1, self.ws.OP_TEXT, message1)
+ self.ws.frame_write(sock2, self.ws.OP_TEXT, message2)
+
+ frame1 = self.ws.frame_read(sock1)
+ frame2 = self.ws.frame_read(sock2)
+
+ self.assertEqual(message1, frame1['data'].decode('utf-8'), 'client 1')
+ self.assertEqual(message2, frame2['data'].decode('utf-8'), 'client 2')
+
+ sock1.close()
+ sock2.close()
+
+ @unittest.skip('not yet')
+ def test_java_websockets_handshake_upgrade_absent(
+ self
+ ): # FAIL https://tools.ietf.org/html/rfc6455#section-4.2.1
+ self.load('websockets_mirror')
+
+ self.get()
+
+ key = self.ws.key()
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': key,
+ 'Sec-WebSocket-Protocol': 'chat',
+ 'Sec-WebSocket-Version': 13,
+ },
+ read_timeout=1,
+ )
+
+ self.assertEqual(resp['status'], 400, 'upgrade absent')
+
+ def test_java_websockets_handshake_case_insensitive(self):
+ self.load('websockets_mirror')
+
+ self.get()
+
+ key = self.ws.key()
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'WEBSOCKET',
+ 'Connection': 'UPGRADE',
+ 'Sec-WebSocket-Key': key,
+ 'Sec-WebSocket-Protocol': 'chat',
+ 'Sec-WebSocket-Version': 13,
+ },
+ read_timeout=1,
+ )
+
+ self.assertEqual(resp['status'], 101, 'status')
+
+ @unittest.skip('not yet')
+ def test_java_websockets_handshake_connection_absent(self): # FAIL
+ self.load('websockets_mirror')
+
+ self.get()
+
+ key = self.ws.key()
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'websocket',
+ 'Sec-WebSocket-Key': key,
+ 'Sec-WebSocket-Protocol': 'chat',
+ 'Sec-WebSocket-Version': 13,
+ },
+ read_timeout=1,
+ )
+
+ self.assertEqual(resp['status'], 400, 'status')
+
+ def test_java_websockets_handshake_version_absent(self):
+ self.load('websockets_mirror')
+
+ self.get()
+
+ key = self.ws.key()
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': key,
+ 'Sec-WebSocket-Protocol': 'chat',
+ },
+ read_timeout=1,
+ )
+
+ self.assertEqual(resp['status'], 426, 'status')
+
+ @unittest.skip('not yet')
+ def test_java_websockets_handshake_key_invalid(self):
+ self.load('websockets_mirror')
+
+ self.get()
+
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': '!',
+ 'Sec-WebSocket-Protocol': 'chat',
+ 'Sec-WebSocket-Version': 13,
+ },
+ read_timeout=1,
+ )
+
+ self.assertEqual(resp['status'], 400, 'key length')
+
+ key = self.ws.key()
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': [key, key],
+ 'Sec-WebSocket-Protocol': 'chat',
+ 'Sec-WebSocket-Version': 13,
+ },
+ read_timeout=1,
+ )
+
+ self.assertEqual(
+ resp['status'], 400, 'key double'
+ ) # FAIL https://tools.ietf.org/html/rfc6455#section-11.3.1
+
+ def test_java_websockets_handshake_method_invalid(self):
+ self.load('websockets_mirror')
+
+ self.get()
+
+ key = self.ws.key()
+ resp = self.post(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': key,
+ 'Sec-WebSocket-Protocol': 'chat',
+ 'Sec-WebSocket-Version': 13,
+ },
+ read_timeout=1,
+ )
+
+ self.assertEqual(resp['status'], 400, 'status')
+
+ def test_java_websockets_handshake_http_10(self):
+ self.load('websockets_mirror')
+
+ self.get()
+
+ key = self.ws.key()
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': key,
+ 'Sec-WebSocket-Protocol': 'chat',
+ 'Sec-WebSocket-Version': 13,
+ },
+ http_10=True,
+ read_timeout=1,
+ )
+
+ self.assertEqual(resp['status'], 400, 'status')
+
+ def test_java_websockets_handshake_uri_invalid(self):
+ self.load('websockets_mirror')
+
+ self.get()
+
+ key = self.ws.key()
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': key,
+ 'Sec-WebSocket-Protocol': 'chat',
+ 'Sec-WebSocket-Version': 13,
+ },
+ url='!',
+ read_timeout=1,
+ )
+
+ self.assertEqual(resp['status'], 400, 'status')
+
+ def test_java_websockets_protocol_absent(self):
+ self.load('websockets_mirror')
+
+ self.get()
+
+ key = self.ws.key()
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': key,
+ 'Sec-WebSocket-Version': 13,
+ },
+ read_timeout=1,
+ )
+
+ self.assertEqual(resp['status'], 101, 'status')
+ self.assertEqual(resp['headers']['Upgrade'], 'websocket', 'upgrade')
+ self.assertEqual(
+ resp['headers']['Connection'], 'Upgrade', 'connection'
+ )
+ self.assertEqual(
+ resp['headers']['Sec-WebSocket-Accept'], self.ws.accept(key), 'key'
+ )
+
+ # autobahn-testsuite
+ #
+ # Some following tests fail because of Unit does not support UTF-8
+ # validation for websocket frames. It should be implemented
+ # by application, if necessary.
+
+ def test_java_websockets_1_1_1__1_1_8(self):
+ self.load('websockets_mirror')
+
+ opcode = self.ws.OP_TEXT
+
+ _, sock, _ = self.ws.upgrade()
+
+ def check_length(length, chopsize=None):
+ payload = '*' * length
+
+ self.ws.frame_write(sock, opcode, payload, chopsize=chopsize)
+
+ frame = self.ws.message_read(sock)
+ self.check_frame(frame, True, opcode, payload)
+
+ check_length(0) # 1_1_1
+ check_length(125) # 1_1_2
+ check_length(126) # 1_1_3
+ check_length(127) # 1_1_4
+ check_length(128) # 1_1_5
+ check_length(65535) # 1_1_6
+ check_length(65536) # 1_1_7
+ check_length(65536, chopsize = 997) # 1_1_8
+
+ self.close_connection(sock)
+
+ def test_java_websockets_1_2_1__1_2_8(self):
+ self.load('websockets_mirror')
+
+ opcode = self.ws.OP_BINARY
+
+ _, sock, _ = self.ws.upgrade()
+
+ def check_length(length, chopsize=None):
+ payload = b'\xfe' * length
+
+ self.ws.frame_write(sock, opcode, payload, chopsize=chopsize)
+
+ frame = self.ws.message_read(sock)
+ self.check_frame(frame, True, opcode, payload)
+
+ check_length(0) # 1_2_1
+ check_length(125) # 1_2_2
+ check_length(126) # 1_2_3
+ check_length(127) # 1_2_4
+ check_length(128) # 1_2_5
+ check_length(65535) # 1_2_6
+ check_length(65536) # 1_2_7
+ check_length(65536, chopsize = 997) # 1_2_8
+
+ self.close_connection(sock)
+
+ def test_java_websockets_2_1__2_6(self):
+ self.load('websockets_mirror')
+
+ op_ping = self.ws.OP_PING
+ op_pong = self.ws.OP_PONG
+
+ _, sock, _ = self.ws.upgrade()
+
+ def check_ping(payload, chopsize=None, decode=True):
+ self.ws.frame_write(sock, op_ping, payload, chopsize=chopsize)
+ frame = self.ws.frame_read(sock)
+
+ self.check_frame(frame, True, op_pong, payload, decode=decode)
+
+ check_ping('') # 2_1
+ check_ping('Hello, world!') # 2_2
+ check_ping(b'\x00\xff\xfe\xfd\xfc\xfb\x00\xff', decode=False) # 2_3
+ check_ping(b'\xfe' * 125, decode=False) # 2_4
+ check_ping(b'\xfe' * 125, chopsize=1, decode=False) # 2_6
+
+ self.close_connection(sock)
+
+ # 2_5
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_PING, b'\xfe' * 126)
+ self.check_close(sock, 1002)
+
+ def test_java_websockets_2_7__2_9(self):
+ self.load('websockets_mirror')
+
+ # 2_7
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_PONG, '')
+ self.assertEqual(self.recvall(sock, read_timeout=1), b'', '2_7')
+
+ # 2_8
+
+ self.ws.frame_write(sock, self.ws.OP_PONG, 'unsolicited pong payload')
+ self.assertEqual(self.recvall(sock, read_timeout=1), b'', '2_8')
+
+ # 2_9
+
+ payload = 'ping payload'
+
+ self.ws.frame_write(sock, self.ws.OP_PONG, 'unsolicited pong payload')
+ self.ws.frame_write(sock, self.ws.OP_PING, payload)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_PONG, payload)
+
+ self.close_connection(sock)
+
+ def test_java_websockets_2_10__2_11(self):
+ self.load('websockets_mirror')
+
+ # 2_10
+
+ _, sock, _ = self.ws.upgrade()
+
+ for i in range(0, 10):
+ self.ws.frame_write(sock, self.ws.OP_PING, 'payload-%d' % i)
+
+ for i in range(0, 10):
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_PONG, 'payload-%d' % i)
+
+ # 2_11
+
+ for i in range(0, 10):
+ opcode = self.ws.OP_PING
+ self.ws.frame_write(sock, opcode, 'payload-%d' % i, chopsize=1)
+
+ for i in range(0, 10):
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_PONG, 'payload-%d' % i)
+
+ self.close_connection(sock)
+
+ @unittest.skip('not yet')
+ def test_java_websockets_3_1__3_7(self):
+ self.load('websockets_mirror')
+
+ payload = 'Hello, world!'
+
+ # 3_1
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, payload, rsv1=True)
+ self.check_close(sock, 1002)
+
+ # 3_2
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, payload)
+ self.ws.frame_write(sock, self.ws.OP_TEXT, payload, rsv2=True)
+ self.ws.frame_write(sock, self.ws.OP_PING, '')
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, payload)
+
+ self.check_close(sock, 1002, no_close=True)
+
+ self.assertEqual(self.recvall(sock, read_timeout=1), b'', 'empty 3_2')
+ sock.close()
+
+ # 3_3
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, payload)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, payload)
+
+ self.ws.frame_write(
+ sock, self.ws.OP_TEXT, payload, rsv1=True, rsv2=True
+ )
+
+ self.check_close(sock, 1002, no_close=True)
+
+ self.assertEqual(self.recvall(sock, read_timeout=1), b'', 'empty 3_3')
+ sock.close()
+
+ # 3_4
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, payload, chopsize=1)
+ self.ws.frame_write(
+ sock, self.ws.OP_TEXT, payload, rsv3=True, chopsize=1
+ )
+ self.ws.frame_write(sock, self.ws.OP_PING, '')
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, payload)
+
+ self.check_close(sock, 1002, no_close=True)
+
+ self.assertEqual(self.recvall(sock, read_timeout=1), b'', 'empty 3_4')
+ sock.close()
+
+ # 3_5
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(
+ sock,
+ self.ws.OP_BINARY,
+ b'\x00\xff\xfe\xfd\xfc\xfb\x00\xff',
+ rsv1=True,
+ rsv3=True,
+ )
+
+ self.check_close(sock, 1002)
+
+ # 3_6
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(
+ sock, self.ws.OP_PING, payload, rsv2=True, rsv3=True
+ )
+
+ self.check_close(sock, 1002)
+
+ # 3_7
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(
+ sock, self.ws.OP_CLOSE, payload, rsv1=True, rsv2=True, rsv3=True
+ )
+
+ self.check_close(sock, 1002)
+
+ def test_java_websockets_4_1_1__4_2_5(self):
+ self.load('websockets_mirror')
+
+ payload = 'Hello, world!'
+
+ # 4_1_1
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, 0x03, '')
+ self.check_close(sock, 1002)
+
+ # 4_1_2
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, 0x04, 'reserved opcode payload')
+ self.check_close(sock, 1002)
+
+ # 4_1_3
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, payload)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, payload)
+
+ self.ws.frame_write(sock, 0x05, '')
+ self.ws.frame_write(sock, self.ws.OP_PING, '')
+
+ self.check_close(sock, 1002)
+
+ # 4_1_4
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, payload)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, payload)
+
+ self.ws.frame_write(sock, 0x06, payload)
+ self.ws.frame_write(sock, self.ws.OP_PING, '')
+
+ self.check_close(sock, 1002)
+
+ # 4_1_5
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, payload, chopsize=1)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, payload)
+
+ self.ws.frame_write(sock, 0x07, payload, chopsize=1)
+ self.ws.frame_write(sock, self.ws.OP_PING, '')
+
+ self.check_close(sock, 1002)
+
+ # 4_2_1
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, 0x0B, '')
+ self.check_close(sock, 1002)
+
+ # 4_2_2
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, 0x0C, 'reserved opcode payload')
+ self.check_close(sock, 1002)
+
+ # 4_2_3
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, payload)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, payload)
+
+ self.ws.frame_write(sock, 0x0D, '')
+ self.ws.frame_write(sock, self.ws.OP_PING, '')
+
+ self.check_close(sock, 1002)
+
+ # 4_2_4
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, payload)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, payload)
+
+ self.ws.frame_write(sock, 0x0E, payload)
+ self.ws.frame_write(sock, self.ws.OP_PING, '')
+
+ self.check_close(sock, 1002)
+
+ # 4_2_5
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, payload, chopsize=1)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, payload)
+
+ self.ws.frame_write(sock, 0x0F, payload, chopsize=1)
+ self.ws.frame_write(sock, self.ws.OP_PING, '')
+
+ self.check_close(sock, 1002)
+
+ def test_java_websockets_5_1__5_20(self):
+ self.load('websockets_mirror')
+
+ # 5_1
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_PING, 'fragment1', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True)
+ self.check_close(sock, 1002)
+
+ # 5_2
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_PONG, 'fragment1', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True)
+ self.check_close(sock, 1002)
+
+ # 5_3
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2')
+
+ # 5_4
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False)
+ self.assertEqual(self.recvall(sock, read_timeout=1), b'', '5_4')
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2')
+
+ # 5_5
+
+ self.ws.frame_write(
+ sock, self.ws.OP_TEXT, 'fragment1', fin=False, chopsize=1
+ )
+ self.ws.frame_write(
+ sock, self.ws.OP_CONT, 'fragment2', fin=True, chopsize=1
+ )
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2')
+
+ # 5_6
+
+ ping_payload = 'ping payload'
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_PING, ping_payload)
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_PONG, ping_payload)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2')
+
+ # 5_7
+
+ ping_payload = 'ping payload'
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False)
+ self.assertEqual(self.recvall(sock, read_timeout=1), b'', '5_7')
+
+ self.ws.frame_write(sock, self.ws.OP_PING, ping_payload)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_PONG, ping_payload)
+
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2')
+
+ # 5_8
+
+ ping_payload = 'ping payload'
+
+ self.ws.frame_write(
+ sock, self.ws.OP_TEXT, 'fragment1', fin=False, chopsize=1
+ )
+ self.ws.frame_write(sock, self.ws.OP_PING, ping_payload, chopsize=1)
+ self.ws.frame_write(
+ sock, self.ws.OP_CONT, 'fragment2', fin=True, chopsize=1
+ )
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_PONG, ping_payload)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, 'fragment1fragment2')
+
+ # 5_9
+
+ self.ws.frame_write(
+ sock, self.ws.OP_CONT, 'non-continuation payload', fin=True
+ )
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'Hello, world!', fin=True)
+ self.check_close(sock, 1002)
+
+ # 5_10
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(
+ sock, self.ws.OP_CONT, 'non-continuation payload', fin=True
+ )
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'Hello, world!', fin=True)
+ self.check_close(sock, 1002)
+
+ # 5_11
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(
+ sock,
+ self.ws.OP_CONT,
+ 'non-continuation payload',
+ fin=True,
+ chopsize=1,
+ )
+ self.ws.frame_write(
+ sock, self.ws.OP_TEXT, 'Hello, world!', fin=True, chopsize=1
+ )
+ self.check_close(sock, 1002)
+
+ # 5_12
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(
+ sock, self.ws.OP_CONT, 'non-continuation payload', fin=False
+ )
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'Hello, world!', fin=True)
+ self.check_close(sock, 1002)
+
+ # 5_13
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(
+ sock, self.ws.OP_CONT, 'non-continuation payload', fin=False
+ )
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'Hello, world!', fin=True)
+ self.check_close(sock, 1002)
+
+ # 5_14
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(
+ sock,
+ self.ws.OP_CONT,
+ 'non-continuation payload',
+ fin=False,
+ chopsize=1,
+ )
+ self.ws.frame_write(
+ sock, self.ws.OP_TEXT, 'Hello, world!', fin=True, chopsize=1
+ )
+ self.check_close(sock, 1002)
+
+ # 5_15
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=True)
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment3', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment4', fin=True)
+ self.check_close(sock, 1002)
+
+ # 5_16
+
+ _, sock, _ = self.ws.upgrade()
+
+ for i in range(0, 2):
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment1', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment2', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment3', fin=True)
+ self.check_close(sock, 1002)
+
+ # 5_17
+
+ _, sock, _ = self.ws.upgrade()
+
+ for i in range(0, 2):
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment1', fin=True)
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment2', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment3', fin=True)
+ self.check_close(sock, 1002)
+
+ # 5_18
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment2')
+ self.check_close(sock, 1002)
+
+ # 5_19
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_PING, 'pongme 1!')
+
+ time.sleep(1)
+
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment3', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment4', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_PING, 'pongme 2!')
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment5')
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_PONG, 'pongme 1!')
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_PONG, 'pongme 2!')
+
+ self.check_frame(
+ self.ws.frame_read(sock),
+ True,
+ self.ws.OP_TEXT,
+ 'fragment1fragment2fragment3fragment4fragment5',
+ )
+
+ # 5_20
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_PING, 'pongme 1!')
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_PONG, 'pongme 1!')
+
+ time.sleep(1)
+
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment3', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment4', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_PING, 'pongme 2!')
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_PONG, 'pongme 2!')
+
+ self.assertEqual(self.recvall(sock, read_timeout=1), b'', '5_20')
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment5')
+
+ self.check_frame(
+ self.ws.frame_read(sock),
+ True,
+ self.ws.OP_TEXT,
+ 'fragment1fragment2fragment3fragment4fragment5',
+ )
+
+ self.close_connection(sock)
+
+ def test_java_websockets_6_1_1__6_4_4(self):
+ self.load('websockets_mirror')
+
+ # 6_1_1
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, '')
+ frame = self.ws.frame_read(sock, read_timeout=3)
+ self.check_frame(frame, True, self.ws.OP_TEXT, '')
+
+ # 6_1_2
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, '', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CONT, '', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CONT, '')
+
+ frame = self.ws.frame_read(sock, read_timeout=3)
+ self.check_frame(frame, True, self.ws.OP_TEXT, '')
+
+ # 6_1_3
+
+ payload = 'middle frame payload'
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, '', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CONT, payload, fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CONT, '')
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, payload)
+
+ # 6_2_1
+
+ payload = 'Hello-µ@ßöäüàá-UTF-8!!'
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, payload)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, payload)
+
+ # 6_2_2
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, payload[:12], fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CONT, payload[12:])
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, payload)
+
+ # 6_2_3
+
+ self.ws.message(sock, self.ws.OP_TEXT, payload, fragmention_size=1)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, payload)
+
+ # 6_2_4
+
+ payload = '\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5'
+
+ self.ws.message(sock, self.ws.OP_TEXT, payload, fragmention_size=1)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, payload)
+
+ self.close_connection(sock)
+
+# Unit does not support UTF-8 validation
+#
+# # 6_3_1 FAIL
+#
+# payload_1 = '\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5'
+# payload_2 = '\xed\xa0\x80'
+# payload_3 = '\x65\x64\x69\x74\x65\x64'
+#
+# payload = payload_1 + payload_2 + payload_3
+#
+# self.ws.message(sock, self.ws.OP_TEXT, payload)
+# self.check_close(sock, 1007)
+#
+# # 6_3_2 FAIL
+#
+# _, sock, _ = self.ws.upgrade()
+#
+# self.ws.message(sock, self.ws.OP_TEXT, payload, fragmention_size=1)
+# self.check_close(sock, 1007)
+#
+# # 6_4_1 ... 6_4_4 FAIL
+
+ def test_java_websockets_7_1_1__7_5_1(self):
+ self.load('websockets_mirror')
+
+ # 7_1_1
+
+ _, sock, _ = self.ws.upgrade()
+
+ payload = "Hello World!"
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, payload)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, payload)
+
+ self.close_connection(sock)
+
+ # 7_1_2
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close())
+ self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close())
+
+ self.check_close(sock)
+
+ # 7_1_3
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close())
+ self.check_close(sock, no_close=True)
+
+ self.ws.frame_write(sock, self.ws.OP_PING, '')
+ self.assertEqual(self.recvall(sock, read_timeout=1), b'', 'empty sock')
+
+ sock.close()
+
+ # 7_1_4
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close())
+ self.check_close(sock, no_close=True)
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, payload)
+ self.assertEqual(self.recvall(sock, read_timeout=1), b'', 'empty sock')
+
+ sock.close()
+
+ # 7_1_5
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False)
+ self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close())
+ self.check_close(sock, no_close=True)
+
+ self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2')
+ self.assertEqual(self.recvall(sock, read_timeout=1), b'', 'empty sock')
+
+ sock.close()
+
+ # 7_1_6
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'BAsd7&jh23' * 26 * 2 ** 10)
+ self.ws.frame_write(sock, self.ws.OP_TEXT, payload)
+ self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close())
+
+ self.recvall(sock, read_timeout=1)
+
+ self.ws.frame_write(sock, self.ws.OP_PING, '')
+ self.assertEqual(self.recvall(sock, read_timeout=1), b'', 'empty sock')
+
+ sock.close()
+
+ # 7_3_1 # FAIL
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_CLOSE, '')
+ self.check_close(sock)
+
+ # 7_3_2
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_CLOSE, 'a')
+ self.check_close(sock, 1002)
+
+ # 7_3_3
+
+ _, sock, _ = self.ws.upgrade()
+
+ self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close())
+ self.check_close(sock)
+
+ # 7_3_4
+
+ _, sock, _ = self.ws.upgrade()
+
+ payload = self.ws.serialize_close(reason='Hello World!')
+
+ self.ws.frame_write(sock, self.ws.OP_CLOSE, payload)
+ self.check_close(sock)
+
+ # 7_3_5
+
+ _, sock, _ = self.ws.upgrade()
+
+ payload = self.ws.serialize_close(reason='*' * 123)
+
+ self.ws.frame_write(sock, self.ws.OP_CLOSE, payload)
+ self.check_close(sock)
+
+ # 7_3_6
+
+ _, sock, _ = self.ws.upgrade()
+
+ payload = self.ws.serialize_close(reason='*' * 124)
+
+ self.ws.frame_write(sock, self.ws.OP_CLOSE, payload)
+ self.check_close(sock, 1002)
+
+# # 7_5_1 FAIL Unit does not support UTF-8 validation
+#
+# _, sock, _ = self.ws.upgrade()
+#
+# payload = self.ws.serialize_close(reason = '\xce\xba\xe1\xbd\xb9\xcf' \
+# '\x83\xce\xbc\xce\xb5\xed\xa0\x80\x65\x64\x69\x74\x65\x64')
+#
+# self.ws.frame_write(sock, self.ws.OP_CLOSE, payload)
+# self.check_close(sock, 1007)
+
+ def test_java_websockets_7_7_X__7_9_X(self):
+ self.load('websockets_mirror')
+
+ valid_codes = [
+ 1000,
+ 1001,
+ 1002,
+ 1003,
+ 1007,
+ 1008,
+ 1009,
+ 1010,
+ 1011,
+ 3000,
+ 3999,
+ 4000,
+ 4999,
+ ]
+
+ invalid_codes = [0, 999, 1004, 1005, 1006, 1016, 1100, 2000, 2999]
+
+ for code in valid_codes:
+ _, sock, _ = self.ws.upgrade()
+
+ payload = self.ws.serialize_close(code=code)
+
+ self.ws.frame_write(sock, self.ws.OP_CLOSE, payload)
+ self.check_close(sock, code=code)
+
+ for code in invalid_codes:
+ _, sock, _ = self.ws.upgrade()
+
+ payload = self.ws.serialize_close(code=code)
+
+ self.ws.frame_write(sock, self.ws.OP_CLOSE, payload)
+ self.check_close(sock, 1002)
+
+ def test_java_websockets_7_13_1__7_13_2(self):
+ self.load('websockets_mirror')
+
+ # 7_13_1
+
+ _, sock, _ = self.ws.upgrade()
+
+ payload = self.ws.serialize_close(code=5000)
+
+ self.ws.frame_write(sock, self.ws.OP_CLOSE, payload)
+ self.check_close(sock, 1002)
+
+ # 7_13_2
+
+ _, sock, _ = self.ws.upgrade()
+
+ payload = struct.pack('!I', 65536) + ''.encode('utf-8')
+
+ self.ws.frame_write(sock, self.ws.OP_CLOSE, payload)
+ self.check_close(sock, 1002)
+
+ def test_java_websockets_9_1_1__9_6_6(self):
+ if not self.unsafe:
+ self.skipTest("unsafe, long run")
+
+ self.load('websockets_mirror')
+
+ self.assertIn(
+ 'success',
+ self.conf(
+ {
+ 'http': {
+ 'websocket': {
+ 'max_frame_size': 33554432,
+ 'keepalive_interval': 0,
+ }
+ }
+ },
+ 'settings',
+ ),
+ 'increase max_frame_size and keepalive_interval',
+ )
+
+ _, sock, _ = self.ws.upgrade()
+
+ op_text = self.ws.OP_TEXT
+ op_binary = self.ws.OP_BINARY
+
+ def check_payload(opcode, length, chopsize=None):
+ if opcode == self.ws.OP_TEXT:
+ payload = '*' * length
+ else:
+ payload = b'*' * length
+
+ self.ws.frame_write(sock, opcode, payload, chopsize=chopsize)
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, opcode, payload)
+
+ def check_message(opcode, f_size):
+ if opcode == self.ws.OP_TEXT:
+ payload = '*' * 4 * 2 ** 20
+ else:
+ payload = b'*' * 4 * 2 ** 20
+
+ self.ws.message(sock, opcode, payload, fragmention_size=f_size)
+ frame = self.ws.frame_read(sock, read_timeout=5)
+ self.check_frame(frame, True, opcode, payload)
+
+ check_payload(op_text, 64 * 2 ** 10) # 9_1_1
+ check_payload(op_text, 256 * 2 ** 10) # 9_1_2
+ check_payload(op_text, 2 ** 20) # 9_1_3
+ check_payload(op_text, 4 * 2 ** 20) # 9_1_4
+ check_payload(op_text, 8 * 2 ** 20) # 9_1_5
+ check_payload(op_text, 16 * 2 ** 20) # 9_1_6
+
+ check_payload(op_binary, 64 * 2 ** 10) # 9_2_1
+ check_payload(op_binary, 256 * 2 ** 10) # 9_2_2
+ check_payload(op_binary, 2 ** 20) # 9_2_3
+ check_payload(op_binary, 4 * 2 ** 20) # 9_2_4
+ check_payload(op_binary, 8 * 2 ** 20) # 9_2_5
+ check_payload(op_binary, 16 * 2 ** 20) # 9_2_6
+
+ if self.system != 'Darwin' and self.system != 'FreeBSD':
+ check_message(op_text, 64) # 9_3_1
+ check_message(op_text, 256) # 9_3_2
+ check_message(op_text, 2 ** 10) # 9_3_3
+ check_message(op_text, 4 * 2 ** 10) # 9_3_4
+ check_message(op_text, 16 * 2 ** 10) # 9_3_5
+ check_message(op_text, 64 * 2 ** 10) # 9_3_6
+ check_message(op_text, 256 * 2 ** 10) # 9_3_7
+ check_message(op_text, 2 ** 20) # 9_3_8
+ check_message(op_text, 4 * 2 ** 20) # 9_3_9
+
+ check_message(op_binary, 64) # 9_4_1
+ check_message(op_binary, 256) # 9_4_2
+ check_message(op_binary, 2 ** 10) # 9_4_3
+ check_message(op_binary, 4 * 2 ** 10) # 9_4_4
+ check_message(op_binary, 16 * 2 ** 10) # 9_4_5
+ check_message(op_binary, 64 * 2 ** 10) # 9_4_6
+ check_message(op_binary, 256 * 2 ** 10) # 9_4_7
+ check_message(op_binary, 2 ** 20) # 9_4_8
+ check_message(op_binary, 4 * 2 ** 20) # 9_4_9
+
+ check_payload(op_text, 2 ** 20, chopsize=64) # 9_5_1
+ check_payload(op_text, 2 ** 20, chopsize=128) # 9_5_2
+ check_payload(op_text, 2 ** 20, chopsize=256) # 9_5_3
+ check_payload(op_text, 2 ** 20, chopsize=512) # 9_5_4
+ check_payload(op_text, 2 ** 20, chopsize=1024) # 9_5_5
+ check_payload(op_text, 2 ** 20, chopsize=2048) # 9_5_6
+
+ check_payload(op_binary, 2 ** 20, chopsize=64) # 9_6_1
+ check_payload(op_binary, 2 ** 20, chopsize=128) # 9_6_2
+ check_payload(op_binary, 2 ** 20, chopsize=256) # 9_6_3
+ check_payload(op_binary, 2 ** 20, chopsize=512) # 9_6_4
+ check_payload(op_binary, 2 ** 20, chopsize=1024) # 9_6_5
+ check_payload(op_binary, 2 ** 20, chopsize=2048) # 9_6_6
+
+ self.close_connection(sock)
+
+ def test_java_websockets_10_1_1(self):
+ self.load('websockets_mirror')
+
+ _, sock, _ = self.ws.upgrade()
+
+ payload = '*' * 65536
+
+ self.ws.message(sock, self.ws.OP_TEXT, payload, fragmention_size=1300)
+
+ frame = self.ws.message_read(sock)
+ self.check_frame(frame, True, self.ws.OP_TEXT, payload)
+
+ self.close_connection(sock)
+
+ # settings
+
+ def test_java_websockets_max_frame_size(self):
+ self.load('websockets_mirror')
+
+ self.assertIn(
+ 'success',
+ self.conf(
+ {'http': {'websocket': {'max_frame_size': 100}}}, 'settings'
+ ),
+ 'configure max_frame_size',
+ )
+
+ _, sock, _ = self.ws.upgrade()
+
+ payload = '*' * 94
+ opcode = self.ws.OP_TEXT
+
+ self.ws.frame_write(sock, opcode, payload) # frame length is 100
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, opcode, payload)
+
+ payload = '*' * 95
+
+ self.ws.frame_write(sock, opcode, payload) # frame length is 101
+ self.check_close(sock, 1009) # 1009 - CLOSE_TOO_LARGE
+
+ def test_java_websockets_read_timeout(self):
+ self.load('websockets_mirror')
+
+ self.assertIn(
+ 'success',
+ self.conf(
+ {'http': {'websocket': {'read_timeout': 5}}}, 'settings'
+ ),
+ 'configure read_timeout',
+ )
+
+ _, sock, _ = self.ws.upgrade()
+
+ frame = self.ws.frame_to_send(self.ws.OP_TEXT, 'blah')
+ sock.sendall(frame[:2])
+
+ time.sleep(2)
+
+ self.check_close(sock, 1001) # 1001 - CLOSE_GOING_AWAY
+
+ def test_java_websockets_keepalive_interval(self):
+ self.load('websockets_mirror')
+
+ self.assertIn(
+ 'success',
+ self.conf(
+ {'http': {'websocket': {'keepalive_interval': 5}}}, 'settings'
+ ),
+ 'configure keepalive_interval',
+ )
+
+ _, sock, _ = self.ws.upgrade()
+
+ frame = self.ws.frame_to_send(self.ws.OP_TEXT, 'blah')
+ sock.sendall(frame[:2])
+
+ time.sleep(2)
+
+ frame = self.ws.frame_read(sock)
+ self.check_frame(frame, True, self.ws.OP_PING, '') # PING frame
+
+ sock.close()
+
+
+if __name__ == '__main__':
+ TestJavaWebsockets.main()
diff --git a/test/test_node_application.py b/test/test_node_application.py
index 0354c978..a5b4a108 100644
--- a/test/test_node_application.py
+++ b/test/test_node_application.py
@@ -3,7 +3,7 @@ from unit.applications.lang.node import TestApplicationNode
class TestNodeApplication(TestApplicationNode):
- prerequisites = ['node']
+ prerequisites = {'modules': ['node']}
def test_node_application_basic(self):
self.load('basic')
diff --git a/test/test_node_websockets.py b/test/test_node_websockets.py
index 6652d8c5..b24bee75 100644
--- a/test/test_node_websockets.py
+++ b/test/test_node_websockets.py
@@ -4,8 +4,9 @@ import unittest
from unit.applications.lang.node import TestApplicationNode
from unit.applications.websockets import TestApplicationWebsocket
+
class TestNodeWebsockets(TestApplicationNode):
- prerequisites = ['node']
+ prerequisites = {'modules': ['node']}
ws = TestApplicationWebsocket()
@@ -21,10 +22,7 @@ class TestNodeWebsockets(TestApplicationNode):
)
self.skip_alerts.extend(
- [
- r'last message send failed',
- r'socket close\(\d+\) failed',
- ]
+ [r'last message send failed', r'socket close\(\d+\) failed']
)
def close_connection(self, sock):
@@ -34,7 +32,7 @@ class TestNodeWebsockets(TestApplicationNode):
self.check_close(sock)
- def check_close(self, sock, code = 1000, no_close = False):
+ def check_close(self, sock, code=1000, no_close=False):
frame = self.ws.frame_read(sock)
self.assertEqual(frame['fin'], True, 'close fin')
@@ -61,9 +59,7 @@ class TestNodeWebsockets(TestApplicationNode):
sock.close()
self.assertEqual(resp['status'], 101, 'status')
- self.assertEqual(
- resp['headers']['Upgrade'], 'websocket', 'upgrade'
- )
+ self.assertEqual(resp['headers']['Upgrade'], 'websocket', 'upgrade')
self.assertEqual(
resp['headers']['Connection'], 'Upgrade', 'connection'
)
@@ -81,16 +77,12 @@ class TestNodeWebsockets(TestApplicationNode):
self.ws.frame_write(sock, self.ws.OP_TEXT, message)
frame = self.ws.frame_read(sock)
- self.assertEqual(
- message, frame['data'].decode('utf-8'), 'mirror'
- )
+ self.assertEqual(message, frame['data'].decode('utf-8'), 'mirror')
self.ws.frame_write(sock, self.ws.OP_TEXT, message)
frame = self.ws.frame_read(sock)
- self.assertEqual(
- message, frame['data'].decode('utf-8'), 'mirror 2'
- )
+ self.assertEqual(message, frame['data'].decode('utf-8'), 'mirror 2')
sock.close()
@@ -160,29 +152,6 @@ class TestNodeWebsockets(TestApplicationNode):
sock.close()
- def test_node_websockets_partial_send(self):
- self.load('websockets/mirror')
-
- message = 'blah'
-
- _, sock, _ = self.ws.upgrade()
-
- frame = self.ws.frame_to_send(self.ws.OP_TEXT, message)
- sock.sendall(frame[:1])
- sock.sendall(frame[1:2])
- sock.sendall(frame[2:3])
- sock.sendall(frame[3:])
-
- frame = self.ws.frame_read(sock)
-
- self.assertEqual(
- message,
- frame['data'].decode('utf-8'),
- 'partial send',
- )
-
- sock.close()
-
def test_node_websockets_large(self):
self.load('websockets/mirror_fragmentation')
@@ -202,65 +171,6 @@ class TestNodeWebsockets(TestApplicationNode):
sock.close()
- def test_node_websockets_frame_invalid_opcode(self):
- self.load('websockets/mirror')
-
- message = 'blah'
-
- _, sock, _ = self.ws.upgrade()
-
- self.ws.frame_write(sock, self.ws.OP_TEXT, message, fin=False)
- self.ws.frame_write(sock, self.ws.OP_TEXT, message)
-
- frame = self.ws.frame_read(sock)
-
- frame.pop('data')
- frame.pop('reason')
- self.assertDictEqual(
- frame,
- {
- 'fin': True,
- 'rsv1': False,
- 'rsv2': False,
- 'rsv3': False,
- 'opcode': self.ws.OP_CLOSE,
- 'mask': 0,
- 'code': 1002,
- },
- 'close frame',
- )
-
- sock.close()
-
- def test_node_websockets_frame_invalid_opcode_2(self):
- self.load('websockets/mirror')
-
- message = 'blah'
-
- _, sock, _ = self.ws.upgrade()
-
- self.ws.frame_write(sock, self.ws.OP_CONT, message)
-
- frame = self.ws.frame_read(sock)
-
- frame.pop('data')
- self.assertDictEqual(
- frame,
- {
- 'fin': True,
- 'rsv1': False,
- 'rsv2': False,
- 'rsv3': False,
- 'opcode': self.ws.OP_CLOSE,
- 'mask': 0,
- 'code': 1002,
- 'reason': 'Unrecognized opcode 0',
- },
- 'close frame',
- )
-
- sock.close()
-
def test_node_websockets_two_clients(self):
self.load('websockets/mirror')
@@ -276,28 +186,29 @@ class TestNodeWebsockets(TestApplicationNode):
frame1 = self.ws.frame_read(sock1)
frame2 = self.ws.frame_read(sock2)
- self.assertEqual(
- message1, frame1['data'].decode('utf-8'), 'client 1'
- )
- self.assertEqual(
- message2, frame2['data'].decode('utf-8'), 'client 2'
- )
+ self.assertEqual(message1, frame1['data'].decode('utf-8'), 'client 1')
+ self.assertEqual(message2, frame2['data'].decode('utf-8'), 'client 2')
sock1.close()
sock2.close()
@unittest.skip('not yet')
- def test_node_websockets_handshake_upgrade_absent(self): # FAIL https://tools.ietf.org/html/rfc6455#section-4.2.1
+ def test_node_websockets_handshake_upgrade_absent(
+ self
+ ): # FAIL https://tools.ietf.org/html/rfc6455#section-4.2.1
self.load('websockets/mirror')
key = self.ws.key()
- resp = self.get(headers={
- 'Host': 'localhost',
- 'Connection': 'Upgrade',
- 'Sec-WebSocket-Key': key,
- 'Sec-WebSocket-Protocol': 'chat',
- 'Sec-WebSocket-Version': 13,
- }, read_timeout=1)
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': key,
+ 'Sec-WebSocket-Protocol': 'chat',
+ 'Sec-WebSocket-Version': 13,
+ },
+ read_timeout=1,
+ )
self.assertEqual(resp['status'], 400, 'upgrade absent')
@@ -305,29 +216,35 @@ class TestNodeWebsockets(TestApplicationNode):
self.load('websockets/mirror')
key = self.ws.key()
- resp = self.get(headers={
- 'Host': 'localhost',
- 'Upgrade': 'WEBSOCKET',
- 'Connection': 'UPGRADE',
- 'Sec-WebSocket-Key': key,
- 'Sec-WebSocket-Protocol': 'chat',
- 'Sec-WebSocket-Version': 13,
- }, read_timeout=1)
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'WEBSOCKET',
+ 'Connection': 'UPGRADE',
+ 'Sec-WebSocket-Key': key,
+ 'Sec-WebSocket-Protocol': 'chat',
+ 'Sec-WebSocket-Version': 13,
+ },
+ read_timeout=1,
+ )
self.assertEqual(resp['status'], 101, 'status')
@unittest.skip('not yet')
- def test_node_websockets_handshake_connection_absent(self): # FAIL
+ def test_node_websockets_handshake_connection_absent(self): # FAIL
self.load('websockets/mirror')
key = self.ws.key()
- resp = self.get(headers={
- 'Host': 'localhost',
- 'Upgrade': 'websocket',
- 'Sec-WebSocket-Key': key,
- 'Sec-WebSocket-Protocol': 'chat',
- 'Sec-WebSocket-Version': 13,
- }, read_timeout=1)
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'websocket',
+ 'Sec-WebSocket-Key': key,
+ 'Sec-WebSocket-Protocol': 'chat',
+ 'Sec-WebSocket-Version': 13,
+ },
+ read_timeout=1,
+ )
self.assertEqual(resp['status'], 400, 'status')
@@ -335,13 +252,16 @@ class TestNodeWebsockets(TestApplicationNode):
self.load('websockets/mirror')
key = self.ws.key()
- resp = self.get(headers={
- 'Host': 'localhost',
- 'Upgrade': 'websocket',
- 'Connection': 'Upgrade',
- 'Sec-WebSocket-Key': key,
- 'Sec-WebSocket-Protocol': 'chat'
- }, read_timeout=1)
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': key,
+ 'Sec-WebSocket-Protocol': 'chat',
+ },
+ read_timeout=1,
+ )
self.assertEqual(resp['status'], 426, 'status')
@@ -349,41 +269,52 @@ class TestNodeWebsockets(TestApplicationNode):
def test_node_websockets_handshake_key_invalid(self):
self.load('websockets/mirror')
- resp = self.get(headers={
- 'Host': 'localhost',
- 'Upgrade': 'websocket',
- 'Connection': 'Upgrade',
- 'Sec-WebSocket-Key': '!',
- 'Sec-WebSocket-Protocol': 'chat',
- 'Sec-WebSocket-Version': 13
- }, read_timeout=1)
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': '!',
+ 'Sec-WebSocket-Protocol': 'chat',
+ 'Sec-WebSocket-Version': 13,
+ },
+ read_timeout=1,
+ )
self.assertEqual(resp['status'], 400, 'key length')
key = self.ws.key()
- resp = self.get(headers={
- 'Host': 'localhost',
- 'Upgrade': 'websocket',
- 'Connection': 'Upgrade',
- 'Sec-WebSocket-Key': [key, key],
- 'Sec-WebSocket-Protocol': 'chat',
- 'Sec-WebSocket-Version': 13
- }, read_timeout=1)
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': [key, key],
+ 'Sec-WebSocket-Protocol': 'chat',
+ 'Sec-WebSocket-Version': 13,
+ },
+ read_timeout=1,
+ )
- self.assertEqual(resp['status'], 400, 'key double') # FAIL https://tools.ietf.org/html/rfc6455#section-11.3.1
+ self.assertEqual(
+ resp['status'], 400, 'key double'
+ ) # FAIL https://tools.ietf.org/html/rfc6455#section-11.3.1
def test_node_websockets_handshake_method_invalid(self):
self.load('websockets/mirror')
key = self.ws.key()
- resp = self.post(headers={
- 'Host': 'localhost',
- 'Upgrade': 'websocket',
- 'Connection': 'Upgrade',
- 'Sec-WebSocket-Key': key,
- 'Sec-WebSocket-Protocol': 'chat',
- 'Sec-WebSocket-Version': 13
- }, read_timeout=1)
+ resp = self.post(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': key,
+ 'Sec-WebSocket-Protocol': 'chat',
+ 'Sec-WebSocket-Version': 13,
+ },
+ read_timeout=1,
+ )
self.assertEqual(resp['status'], 400, 'status')
@@ -391,14 +322,18 @@ class TestNodeWebsockets(TestApplicationNode):
self.load('websockets/mirror')
key = self.ws.key()
- resp = self.get(headers={
- 'Host': 'localhost',
- 'Upgrade': 'websocket',
- 'Connection': 'Upgrade',
- 'Sec-WebSocket-Key': key,
- 'Sec-WebSocket-Protocol': 'chat',
- 'Sec-WebSocket-Version': 13
- }, http_10=True, read_timeout=1)
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': key,
+ 'Sec-WebSocket-Protocol': 'chat',
+ 'Sec-WebSocket-Version': 13,
+ },
+ http_10=True,
+ read_timeout=1,
+ )
self.assertEqual(resp['status'], 400, 'status')
@@ -406,14 +341,18 @@ class TestNodeWebsockets(TestApplicationNode):
self.load('websockets/mirror')
key = self.ws.key()
- resp = self.get(headers={
- 'Host': 'localhost',
- 'Upgrade': 'websocket',
- 'Connection': 'Upgrade',
- 'Sec-WebSocket-Key': key,
- 'Sec-WebSocket-Protocol': 'chat',
- 'Sec-WebSocket-Version': 13
- }, url='!', read_timeout=1)
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': key,
+ 'Sec-WebSocket-Protocol': 'chat',
+ 'Sec-WebSocket-Version': 13,
+ },
+ url='!',
+ read_timeout=1,
+ )
self.assertEqual(resp['status'], 400, 'status')
@@ -421,18 +360,19 @@ class TestNodeWebsockets(TestApplicationNode):
self.load('websockets/mirror')
key = self.ws.key()
- resp = self.get(headers={
- 'Host': 'localhost',
- 'Upgrade': 'websocket',
- 'Connection': 'Upgrade',
- 'Sec-WebSocket-Key': key,
- 'Sec-WebSocket-Version': 13
- }, read_timeout=1)
+ resp = self.get(
+ headers={
+ 'Host': 'localhost',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': key,
+ 'Sec-WebSocket-Version': 13,
+ },
+ read_timeout=1,
+ )
self.assertEqual(resp['status'], 101, 'status')
- self.assertEqual(
- resp['headers']['Upgrade'], 'websocket', 'upgrade'
- )
+ self.assertEqual(resp['headers']['Upgrade'], 'websocket', 'upgrade')
self.assertEqual(
resp['headers']['Connection'], 'Upgrade', 'connection'
)
@@ -441,12 +381,11 @@ class TestNodeWebsockets(TestApplicationNode):
)
# autobahn-testsuite
-
+ #
# Some following tests fail because of Unit does not support UTF-8
# validation for websocket frames. It should be implemented
# by application, if necessary.
- @unittest.skip('not yet')
def test_node_websockets_1_1_1__1_1_8(self):
self.load('websockets/mirror')
@@ -473,7 +412,6 @@ class TestNodeWebsockets(TestApplicationNode):
self.close_connection(sock)
- @unittest.skip('not yet')
def test_node_websockets_1_2_1__1_2_8(self):
self.load('websockets/mirror')
@@ -606,7 +544,7 @@ class TestNodeWebsockets(TestApplicationNode):
frame = self.ws.frame_read(sock)
self.check_frame(frame, True, self.ws.OP_TEXT, payload)
- self.check_close(sock, 1002, no_close = True)
+ self.check_close(sock, 1002, no_close=True)
self.assertEqual(self.recvall(sock, read_timeout=1), b'', 'empty 3_2')
sock.close()
@@ -621,14 +559,10 @@ class TestNodeWebsockets(TestApplicationNode):
self.check_frame(frame, True, self.ws.OP_TEXT, payload)
self.ws.frame_write(
- sock,
- self.ws.OP_TEXT,
- payload,
- rsv1=True,
- rsv2=True,
+ sock, self.ws.OP_TEXT, payload, rsv1=True, rsv2=True
)
- self.check_close(sock, 1002, no_close = True)
+ self.check_close(sock, 1002, no_close=True)
self.assertEqual(self.recvall(sock, read_timeout=1), b'', 'empty 3_3')
sock.close()
@@ -639,18 +573,14 @@ class TestNodeWebsockets(TestApplicationNode):
self.ws.frame_write(sock, self.ws.OP_TEXT, payload, chopsize=1)
self.ws.frame_write(
- sock,
- self.ws.OP_TEXT,
- payload,
- rsv3=True,
- chopsize=1
+ sock, self.ws.OP_TEXT, payload, rsv3=True, chopsize=1
)
self.ws.frame_write(sock, self.ws.OP_PING, '')
frame = self.ws.frame_read(sock)
self.check_frame(frame, True, self.ws.OP_TEXT, payload)
- self.check_close(sock, 1002, no_close = True)
+ self.check_close(sock, 1002, no_close=True)
self.assertEqual(self.recvall(sock, read_timeout=1), b'', 'empty 3_4')
sock.close()
@@ -674,11 +604,7 @@ class TestNodeWebsockets(TestApplicationNode):
_, sock, _ = self.ws.upgrade()
self.ws.frame_write(
- sock,
- self.ws.OP_PING,
- payload,
- rsv2=True,
- rsv3=True,
+ sock, self.ws.OP_PING, payload, rsv2=True, rsv3=True
)
self.check_close(sock, 1002)
@@ -688,12 +614,7 @@ class TestNodeWebsockets(TestApplicationNode):
_, sock, _ = self.ws.upgrade()
self.ws.frame_write(
- sock,
- self.ws.OP_CLOSE,
- payload,
- rsv1=True,
- rsv2=True,
- rsv3=True,
+ sock, self.ws.OP_CLOSE, payload, rsv1=True, rsv2=True, rsv3=True
)
self.check_close(sock, 1002)
@@ -815,7 +736,6 @@ class TestNodeWebsockets(TestApplicationNode):
self.check_close(sock, 1002)
- @unittest.skip('not yet')
def test_node_websockets_5_1__5_20(self):
self.load('websockets/mirror')
@@ -857,18 +777,10 @@ class TestNodeWebsockets(TestApplicationNode):
# 5_5
self.ws.frame_write(
- sock,
- self.ws.OP_TEXT,
- 'fragment1',
- fin=False,
- chopsize=1,
+ sock, self.ws.OP_TEXT, 'fragment1', fin=False, chopsize=1
)
self.ws.frame_write(
- sock,
- self.ws.OP_CONT,
- 'fragment2',
- fin=True,
- chopsize=1,
+ sock, self.ws.OP_CONT, 'fragment2', fin=True, chopsize=1
)
frame = self.ws.frame_read(sock)
@@ -910,19 +822,11 @@ class TestNodeWebsockets(TestApplicationNode):
ping_payload = 'ping payload'
self.ws.frame_write(
- sock,
- self.ws.OP_TEXT,
- 'fragment1',
- fin=False,
- chopsize=1,
+ sock, self.ws.OP_TEXT, 'fragment1', fin=False, chopsize=1
)
self.ws.frame_write(sock, self.ws.OP_PING, ping_payload, chopsize=1)
self.ws.frame_write(
- sock,
- self.ws.OP_CONT,
- 'fragment2',
- fin=True,
- chopsize=1,
+ sock, self.ws.OP_CONT, 'fragment2', fin=True, chopsize=1
)
frame = self.ws.frame_read(sock)
@@ -934,10 +838,7 @@ class TestNodeWebsockets(TestApplicationNode):
# 5_9
self.ws.frame_write(
- sock,
- self.ws.OP_CONT,
- 'non-continuation payload',
- fin=True,
+ sock, self.ws.OP_CONT, 'non-continuation payload', fin=True
)
self.ws.frame_write(sock, self.ws.OP_TEXT, 'Hello, world!', fin=True)
self.check_close(sock, 1002)
@@ -947,10 +848,7 @@ class TestNodeWebsockets(TestApplicationNode):
_, sock, _ = self.ws.upgrade()
self.ws.frame_write(
- sock,
- self.ws.OP_CONT,
- 'non-continuation payload',
- fin=True,
+ sock, self.ws.OP_CONT, 'non-continuation payload', fin=True
)
self.ws.frame_write(sock, self.ws.OP_TEXT, 'Hello, world!', fin=True)
self.check_close(sock, 1002)
@@ -967,11 +865,7 @@ class TestNodeWebsockets(TestApplicationNode):
chopsize=1,
)
self.ws.frame_write(
- sock,
- self.ws.OP_TEXT,
- 'Hello, world!',
- fin=True,
- chopsize=1,
+ sock, self.ws.OP_TEXT, 'Hello, world!', fin=True, chopsize=1
)
self.check_close(sock, 1002)
@@ -980,10 +874,7 @@ class TestNodeWebsockets(TestApplicationNode):
_, sock, _ = self.ws.upgrade()
self.ws.frame_write(
- sock,
- self.ws.OP_CONT,
- 'non-continuation payload',
- fin=False,
+ sock, self.ws.OP_CONT, 'non-continuation payload', fin=False
)
self.ws.frame_write(sock, self.ws.OP_TEXT, 'Hello, world!', fin=True)
self.check_close(sock, 1002)
@@ -993,10 +884,7 @@ class TestNodeWebsockets(TestApplicationNode):
_, sock, _ = self.ws.upgrade()
self.ws.frame_write(
- sock,
- self.ws.OP_CONT,
- 'non-continuation payload',
- fin=False,
+ sock, self.ws.OP_CONT, 'non-continuation payload', fin=False
)
self.ws.frame_write(sock, self.ws.OP_TEXT, 'Hello, world!', fin=True)
self.check_close(sock, 1002)
@@ -1013,11 +901,7 @@ class TestNodeWebsockets(TestApplicationNode):
chopsize=1,
)
self.ws.frame_write(
- sock,
- self.ws.OP_TEXT,
- 'Hello, world!',
- fin=True,
- chopsize=1,
+ sock, self.ws.OP_TEXT, 'Hello, world!', fin=True, chopsize=1
)
self.check_close(sock, 1002)
@@ -1183,8 +1067,8 @@ class TestNodeWebsockets(TestApplicationNode):
self.close_connection(sock)
- # Unit does not support UTF-8 validation
-
+# Unit does not support UTF-8 validation
+#
# # 6_3_1 FAIL
#
# payload_1 = '\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5'
@@ -1235,7 +1119,7 @@ class TestNodeWebsockets(TestApplicationNode):
_, sock, _ = self.ws.upgrade()
self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close())
- self.check_close(sock, no_close = True)
+ self.check_close(sock, no_close=True)
self.ws.frame_write(sock, self.ws.OP_PING, '')
self.assertEqual(self.recvall(sock, read_timeout=1), b'', 'empty sock')
@@ -1247,7 +1131,7 @@ class TestNodeWebsockets(TestApplicationNode):
_, sock, _ = self.ws.upgrade()
self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close())
- self.check_close(sock, no_close = True)
+ self.check_close(sock, no_close=True)
self.ws.frame_write(sock, self.ws.OP_TEXT, payload)
self.assertEqual(self.recvall(sock, read_timeout=1), b'', 'empty sock')
@@ -1260,7 +1144,7 @@ class TestNodeWebsockets(TestApplicationNode):
self.ws.frame_write(sock, self.ws.OP_TEXT, 'fragment1', fin=False)
self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close())
- self.check_close(sock, no_close = True)
+ self.check_close(sock, no_close=True)
self.ws.frame_write(sock, self.ws.OP_CONT, 'fragment2')
self.assertEqual(self.recvall(sock, read_timeout=1), b'', 'empty sock')
@@ -1271,7 +1155,7 @@ class TestNodeWebsockets(TestApplicationNode):
_, sock, _ = self.ws.upgrade()
- self.ws.frame_write(sock, self.ws.OP_TEXT, 'BAsd7&jh23' * 26 * 2**10)
+ self.ws.frame_write(sock, self.ws.OP_TEXT, 'BAsd7&jh23' * 26 * 2 ** 10)
self.ws.frame_write(sock, self.ws.OP_TEXT, payload)
self.ws.frame_write(sock, self.ws.OP_CLOSE, self.ws.serialize_close())
@@ -1307,7 +1191,7 @@ class TestNodeWebsockets(TestApplicationNode):
_, sock, _ = self.ws.upgrade()
- payload = self.ws.serialize_close(reason = 'Hello World!')
+ payload = self.ws.serialize_close(reason='Hello World!')
self.ws.frame_write(sock, self.ws.OP_CLOSE, payload)
self.check_close(sock)
@@ -1316,7 +1200,7 @@ class TestNodeWebsockets(TestApplicationNode):
_, sock, _ = self.ws.upgrade()
- payload = self.ws.serialize_close(reason = '*' * 123)
+ payload = self.ws.serialize_close(reason='*' * 123)
self.ws.frame_write(sock, self.ws.OP_CLOSE, payload)
self.check_close(sock)
@@ -1325,13 +1209,13 @@ class TestNodeWebsockets(TestApplicationNode):
_, sock, _ = self.ws.upgrade()
- payload = self.ws.serialize_close(reason = '*' * 124)
+ payload = self.ws.serialize_close(reason='*' * 124)
self.ws.frame_write(sock, self.ws.OP_CLOSE, payload)
self.check_close(sock, 1002)
- # 7_5_1 FAIL Unit does not support UTF-8 validation
-
+# # 7_5_1 FAIL Unit does not support UTF-8 validation
+#
# _, sock, _ = self.ws.upgrade()
#
# payload = self.ws.serialize_close(reason = '\xce\xba\xe1\xbd\xb9\xcf' \
@@ -1364,7 +1248,7 @@ class TestNodeWebsockets(TestApplicationNode):
for code in valid_codes:
_, sock, _ = self.ws.upgrade()
- payload = self.ws.serialize_close(code = code)
+ payload = self.ws.serialize_close(code=code)
self.ws.frame_write(sock, self.ws.OP_CLOSE, payload)
self.check_close(sock)
@@ -1372,7 +1256,7 @@ class TestNodeWebsockets(TestApplicationNode):
for code in invalid_codes:
_, sock, _ = self.ws.upgrade()
- payload = self.ws.serialize_close(code = code)
+ payload = self.ws.serialize_close(code=code)
self.ws.frame_write(sock, self.ws.OP_CLOSE, payload)
self.check_close(sock, 1002)
@@ -1384,7 +1268,7 @@ class TestNodeWebsockets(TestApplicationNode):
_, sock, _ = self.ws.upgrade()
- payload = self.ws.serialize_close(code = 5000)
+ payload = self.ws.serialize_close(code=5000)
self.ws.frame_write(sock, self.ws.OP_CLOSE, payload)
self.check_close(sock, 1002)
@@ -1437,62 +1321,62 @@ class TestNodeWebsockets(TestApplicationNode):
def check_message(opcode, f_size):
if opcode == self.ws.OP_TEXT:
- payload = '*' * 4 * 2**20
+ payload = '*' * 4 * 2 ** 20
else:
- payload = b'*' * 4 * 2**20
+ payload = b'*' * 4 * 2 ** 20
self.ws.message(sock, opcode, payload, fragmention_size=f_size)
frame = self.ws.frame_read(sock, read_timeout=5)
self.check_frame(frame, True, opcode, payload)
- check_payload(op_text, 64 * 2**10) # 9_1_1
- check_payload(op_text, 256 * 2**10) # 9_1_2
- check_payload(op_text, 2**20) # 9_1_3
- check_payload(op_text, 4 * 2**20) # 9_1_4
- check_payload(op_text, 8 * 2**20) # 9_1_5
- check_payload(op_text, 16 * 2**20) # 9_1_6
+ check_payload(op_text, 64 * 2 ** 10) # 9_1_1
+ check_payload(op_text, 256 * 2 ** 10) # 9_1_2
+ check_payload(op_text, 2 ** 20) # 9_1_3
+ check_payload(op_text, 4 * 2 ** 20) # 9_1_4
+ check_payload(op_text, 8 * 2 ** 20) # 9_1_5
+ check_payload(op_text, 16 * 2 ** 20) # 9_1_6
- check_payload(op_binary, 64 * 2**10) # 9_2_1
- check_payload(op_binary, 256 * 2**10) # 9_2_2
- check_payload(op_binary, 2**20) # 9_2_3
- check_payload(op_binary, 4 * 2**20) # 9_2_4
- check_payload(op_binary, 8 * 2**20) # 9_2_5
- check_payload(op_binary, 16 * 2**20) # 9_2_6
+ check_payload(op_binary, 64 * 2 ** 10) # 9_2_1
+ check_payload(op_binary, 256 * 2 ** 10) # 9_2_2
+ check_payload(op_binary, 2 ** 20) # 9_2_3
+ check_payload(op_binary, 4 * 2 ** 20) # 9_2_4
+ check_payload(op_binary, 8 * 2 ** 20) # 9_2_5
+ check_payload(op_binary, 16 * 2 ** 20) # 9_2_6
if self.system != 'Darwin' and self.system != 'FreeBSD':
- check_message(op_text, 64) # 9_3_1
- check_message(op_text, 256) # 9_3_2
- check_message(op_text, 2**10) # 9_3_3
- check_message(op_text, 4 * 2**10) # 9_3_4
- check_message(op_text, 16 * 2**10) # 9_3_5
- check_message(op_text, 64 * 2**10) # 9_3_6
- check_message(op_text, 256 * 2**10) # 9_3_7
- check_message(op_text, 2**20) # 9_3_8
- check_message(op_text, 4 * 2**20) # 9_3_9
-
- check_message(op_binary, 64) # 9_4_1
- check_message(op_binary, 256) # 9_4_2
- check_message(op_binary, 2**10) # 9_4_3
- check_message(op_binary, 4 * 2**10) # 9_4_4
- check_message(op_binary, 16 * 2**10) # 9_4_5
- check_message(op_binary, 64 * 2**10) # 9_4_6
- check_message(op_binary, 256 * 2**10) # 9_4_7
- check_message(op_binary, 2**20) # 9_4_8
- check_message(op_binary, 4 * 2**20) # 9_4_9
-
- check_payload(op_text, 2**20, chopsize=64) # 9_5_1
- check_payload(op_text, 2**20, chopsize=128) # 9_5_2
- check_payload(op_text, 2**20, chopsize=256) # 9_5_3
- check_payload(op_text, 2**20, chopsize=512) # 9_5_4
- check_payload(op_text, 2**20, chopsize=1024) # 9_5_5
- check_payload(op_text, 2**20, chopsize=2048) # 9_5_6
-
- check_payload(op_binary, 2**20, chopsize=64) # 9_6_1
- check_payload(op_binary, 2**20, chopsize=128) # 9_6_2
- check_payload(op_binary, 2**20, chopsize=256) # 9_6_3
- check_payload(op_binary, 2**20, chopsize=512) # 9_6_4
- check_payload(op_binary, 2**20, chopsize=1024) # 9_6_5
- check_payload(op_binary, 2**20, chopsize=2048) # 9_6_6
+ check_message(op_text, 64) # 9_3_1
+ check_message(op_text, 256) # 9_3_2
+ check_message(op_text, 2 ** 10) # 9_3_3
+ check_message(op_text, 4 * 2 ** 10) # 9_3_4
+ check_message(op_text, 16 * 2 ** 10) # 9_3_5
+ check_message(op_text, 64 * 2 ** 10) # 9_3_6
+ check_message(op_text, 256 * 2 ** 10) # 9_3_7
+ check_message(op_text, 2 ** 20) # 9_3_8
+ check_message(op_text, 4 * 2 ** 20) # 9_3_9
+
+ check_message(op_binary, 64) # 9_4_1
+ check_message(op_binary, 256) # 9_4_2
+ check_message(op_binary, 2 ** 10) # 9_4_3
+ check_message(op_binary, 4 * 2 ** 10) # 9_4_4
+ check_message(op_binary, 16 * 2 ** 10) # 9_4_5
+ check_message(op_binary, 64 * 2 ** 10) # 9_4_6
+ check_message(op_binary, 256 * 2 ** 10) # 9_4_7
+ check_message(op_binary, 2 ** 20) # 9_4_8
+ check_message(op_binary, 4 * 2 ** 20) # 9_4_9
+
+ check_payload(op_text, 2 ** 20, chopsize=64) # 9_5_1
+ check_payload(op_text, 2 ** 20, chopsize=128) # 9_5_2
+ check_payload(op_text, 2 ** 20, chopsize=256) # 9_5_3
+ check_payload(op_text, 2 ** 20, chopsize=512) # 9_5_4
+ check_payload(op_text, 2 ** 20, chopsize=1024) # 9_5_5
+ check_payload(op_text, 2 ** 20, chopsize=2048) # 9_5_6
+
+ check_payload(op_binary, 2 ** 20, chopsize=64) # 9_6_1
+ check_payload(op_binary, 2 ** 20, chopsize=128) # 9_6_2
+ check_payload(op_binary, 2 ** 20, chopsize=256) # 9_6_3
+ check_payload(op_binary, 2 ** 20, chopsize=512) # 9_6_4
+ check_payload(op_binary, 2 ** 20, chopsize=1024) # 9_6_5
+ check_payload(op_binary, 2 ** 20, chopsize=2048) # 9_6_6
self.close_connection(sock)
@@ -1536,7 +1420,7 @@ class TestNodeWebsockets(TestApplicationNode):
payload = '*' * 95
self.ws.frame_write(sock, opcode, payload) # frame length is 101
- self.check_close(sock, 1009) # 1009 - CLOSE_TOO_LARGE
+ self.check_close(sock, 1009) # 1009 - CLOSE_TOO_LARGE
def test_node_websockets_read_timeout(self):
self.load('websockets/mirror')
@@ -1556,7 +1440,7 @@ class TestNodeWebsockets(TestApplicationNode):
time.sleep(2)
- self.check_close(sock, 1001) # 1001 - CLOSE_GOING_AWAY
+ self.check_close(sock, 1001) # 1001 - CLOSE_GOING_AWAY
def test_node_websockets_keepalive_interval(self):
self.load('websockets/mirror')
@@ -1581,5 +1465,6 @@ class TestNodeWebsockets(TestApplicationNode):
sock.close()
+
if __name__ == '__main__':
TestNodeWebsockets.main()
diff --git a/test/test_perl_application.py b/test/test_perl_application.py
index bc26b000..bf3c65d5 100644
--- a/test/test_perl_application.py
+++ b/test/test_perl_application.py
@@ -3,7 +3,7 @@ from unit.applications.lang.perl import TestApplicationPerl
class TestPerlApplication(TestApplicationPerl):
- prerequisites = ['perl']
+ prerequisites = {'modules': ['perl']}
def test_perl_application(self):
self.load('variables')
diff --git a/test/test_php_application.py b/test/test_php_application.py
index ee2048b5..d614885c 100644
--- a/test/test_php_application.py
+++ b/test/test_php_application.py
@@ -3,7 +3,7 @@ import unittest
from unit.applications.lang.php import TestApplicationPHP
class TestPHPApplication(TestApplicationPHP):
- prerequisites = ['php']
+ prerequisites = {'modules': ['php']}
def before_disable_functions(self):
body = self.get()['body']
diff --git a/test/test_php_basic.py b/test/test_php_basic.py
index 0c84f206..7ecff1b2 100644
--- a/test/test_php_basic.py
+++ b/test/test_php_basic.py
@@ -2,7 +2,7 @@ from unit.control import TestControl
class TestPHPBasic(TestControl):
- prerequisites = ['php']
+ prerequisites = {'modules': ['php']}
conf_app = {
"app": {
diff --git a/test/test_python_application.py b/test/test_python_application.py
index 3484b25e..5b6e2089 100644
--- a/test/test_python_application.py
+++ b/test/test_python_application.py
@@ -4,7 +4,7 @@ from unit.applications.lang.python import TestApplicationPython
class TestPythonApplication(TestApplicationPython):
- prerequisites = ['python']
+ prerequisites = {'modules': ['python']}
def test_python_application_variables(self):
self.load('variables')
@@ -71,6 +71,37 @@ class TestPythonApplication(TestApplicationPython):
'Query-String header',
)
+ def test_python_application_query_string_space(self):
+ self.load('query_string')
+
+ resp = self.get(url='/ ?var1=val1&var2=val2')
+ self.assertEqual(
+ resp['headers']['Query-String'],
+ 'var1=val1&var2=val2',
+ 'Query-String space',
+ )
+
+ resp = self.get(url='/ %20?var1=val1&var2=val2')
+ self.assertEqual(
+ resp['headers']['Query-String'],
+ 'var1=val1&var2=val2',
+ 'Query-String space 2',
+ )
+
+ resp = self.get(url='/ %20 ?var1=val1&var2=val2')
+ self.assertEqual(
+ resp['headers']['Query-String'],
+ 'var1=val1&var2=val2',
+ 'Query-String space 3',
+ )
+
+ resp = self.get(url='/blah %20 blah? var1= val1 & var2=val2')
+ self.assertEqual(
+ resp['headers']['Query-String'],
+ ' var1= val1 & var2=val2',
+ 'Query-String space 4',
+ )
+
def test_python_application_query_string_empty(self):
self.load('query_string')
diff --git a/test/test_python_basic.py b/test/test_python_basic.py
index e63158e5..67a5f548 100644
--- a/test/test_python_basic.py
+++ b/test/test_python_basic.py
@@ -2,7 +2,7 @@ from unit.control import TestControl
class TestPythonBasic(TestControl):
- prerequisites = ['python']
+ prerequisites = {'modules': ['python']}
conf_app = {
"app": {
diff --git a/test/test_python_environment.py b/test/test_python_environment.py
index 744f4947..fe0baa13 100644
--- a/test/test_python_environment.py
+++ b/test/test_python_environment.py
@@ -2,7 +2,7 @@ from unit.applications.lang.python import TestApplicationPython
class TestPythonEnvironment(TestApplicationPython):
- prerequisites = ['python']
+ prerequisites = {'modules': ['python']}
def test_python_environment_name_null(self):
self.load('environment')
diff --git a/test/test_python_procman.py b/test/test_python_procman.py
index b0c70e53..52d8cacb 100644
--- a/test/test_python_procman.py
+++ b/test/test_python_procman.py
@@ -6,7 +6,7 @@ from unit.applications.lang.python import TestApplicationPython
class TestPythonProcman(TestApplicationPython):
- prerequisites = ['python']
+ prerequisites = {'modules': ['python']}
def pids_for_process(self):
time.sleep(0.2)
diff --git a/test/test_routing.py b/test/test_routing.py
index 6073877d..20e3a1c4 100644
--- a/test/test_routing.py
+++ b/test/test_routing.py
@@ -3,7 +3,7 @@ from unit.applications.proto import TestApplicationProto
class TestRouting(TestApplicationProto):
- prerequisites = ['python']
+ prerequisites = {'modules': ['python']}
def setUp(self):
super().setUp()
diff --git a/test/test_routing_tls.py b/test/test_routing_tls.py
index 433a303e..3df2bc82 100644
--- a/test/test_routing_tls.py
+++ b/test/test_routing_tls.py
@@ -2,7 +2,7 @@ from unit.applications.tls import TestApplicationTLS
class TestRoutingTLS(TestApplicationTLS):
- prerequisites = ['python', 'openssl']
+ prerequisites = {'modules': ['python', 'openssl']}
def test_routes_match_scheme(self):
self.certificate()
diff --git a/test/test_ruby_application.py b/test/test_ruby_application.py
index 67db8a8e..6f82ae81 100644
--- a/test/test_ruby_application.py
+++ b/test/test_ruby_application.py
@@ -3,7 +3,7 @@ from unit.applications.lang.ruby import TestApplicationRuby
class TestRubyApplication(TestApplicationRuby):
- prerequisites = ['ruby']
+ prerequisites = {'modules': ['ruby']}
def test_ruby_application(self):
self.load('variables')
diff --git a/test/test_settings.py b/test/test_settings.py
index 98063440..6b849558 100644
--- a/test/test_settings.py
+++ b/test/test_settings.py
@@ -5,7 +5,7 @@ from unit.applications.lang.python import TestApplicationPython
class TestSettings(TestApplicationPython):
- prerequisites = ['python']
+ prerequisites = {'modules': ['python']}
def test_settings_header_read_timeout(self):
self.load('empty')
diff --git a/test/test_static.py b/test/test_static.py
new file mode 100644
index 00000000..573669a3
--- /dev/null
+++ b/test/test_static.py
@@ -0,0 +1,376 @@
+import os
+import unittest
+from unit.applications.proto import TestApplicationProto
+
+
+class TestStatic(TestApplicationProto):
+ prerequisites = {}
+
+ def setUp(self):
+ super().setUp()
+
+ os.makedirs(self.testdir + '/assets/dir')
+ with open(self.testdir + '/assets/index.html', 'w') as index, \
+ open(self.testdir + '/assets/README', 'w') as readme, \
+ open(self.testdir + '/assets/log.log', 'w') as log, \
+ open(self.testdir + '/assets/dir/file', 'w') as file:
+ index.write('0123456789')
+ readme.write('readme')
+ log.write('[debug]')
+ file.write('blah')
+
+ self._load_conf(
+ {
+ "listeners": {"*:7080": {"pass": "routes"}},
+ "routes": [{"action": {"share": self.testdir + "/assets"}}],
+ "settings": {
+ "http": {
+ "static": {
+ "mime_types": {"text/plain": [".log", "README"]}
+ }
+ }
+ },
+ }
+ )
+
+ def test_static_index(self):
+ self.assertEqual(
+ self.get(url='/index.html')['body'], '0123456789', 'index'
+ )
+ self.assertEqual(self.get(url='/')['body'], '0123456789', 'index 2')
+ self.assertEqual(
+ self.get(url='/dir/')['status'], 404, 'index not found'
+ )
+
+ resp = self.get(url='/index.html/')
+ self.assertEqual(resp['status'], 404, 'index not found 2 status')
+ self.assertEqual(
+ resp['headers']['Content-Type'],
+ 'text/html',
+ 'index not found 2 Content-Type',
+ )
+
+ def test_static_large_file(self):
+ file_size = 32 * 1024 * 1024
+ with open(self.testdir + '/assets/large', 'wb') as f:
+ f.seek(file_size - 1)
+ f.write(b'\0')
+
+ self.assertEqual(
+ len(
+ self.get(url='/large', read_buffer_size=1024 * 1024)['body']
+ ),
+ file_size,
+ 'large file',
+ )
+
+ def test_static_etag(self):
+ etag = self.get(url='/')['headers']['ETag']
+ etag_2 = self.get(url='/README')['headers']['ETag']
+
+ self.assertNotEqual(etag, etag_2, 'different ETag')
+ self.assertEqual(
+ etag, self.get(url='/')['headers']['ETag'], 'same ETag'
+ )
+
+ with open(self.testdir + '/assets/index.html', 'w') as f:
+ f.write('blah')
+
+ self.assertNotEqual(
+ etag, self.get(url='/')['headers']['ETag'], 'new ETag'
+ )
+
+ def test_static_redirect(self):
+ resp = self.get(url='/dir')
+ self.assertEqual(resp['status'], 301, 'redirect status')
+ self.assertEqual(
+ resp['headers']['Location'], '/dir/', 'redirect Location'
+ )
+ self.assertNotIn(
+ 'Content-Type', resp['headers'], 'redirect Content-Type'
+ )
+
+ def test_static_space_in_name(self):
+ os.rename(
+ self.testdir + '/assets/dir/file',
+ self.testdir + '/assets/dir/fi le',
+ )
+ self.waitforfiles(self.testdir + '/assets/dir/fi le')
+ self.assertEqual(
+ self.get(url='/dir/fi le')['body'], 'blah', 'file name'
+ )
+
+ os.rename(self.testdir + '/assets/dir', self.testdir + '/assets/di r')
+ self.waitforfiles(self.testdir + '/assets/di r/fi le')
+ self.assertEqual(
+ self.get(url='/di r/fi le')['body'], 'blah', 'dir name'
+ )
+
+ os.rename(
+ self.testdir + '/assets/di r', self.testdir + '/assets/ di r '
+ )
+ self.waitforfiles(self.testdir + '/assets/ di r /fi le')
+ self.assertEqual(
+ self.get(url='/ di r /fi le')['body'], 'blah', 'dir name enclosing'
+ )
+
+ self.assertEqual(
+ self.get(url='/%20di%20r%20/fi le')['body'], 'blah', 'dir encoded'
+ )
+ self.assertEqual(
+ self.get(url='/ di r %2Ffi le')['body'], 'blah', 'slash encoded'
+ )
+ self.assertEqual(
+ self.get(url='/ di r /fi%20le')['body'], 'blah', 'file encoded'
+ )
+ self.assertEqual(
+ self.get(url='/%20di%20r%20%2Ffi%20le')['body'], 'blah', 'encoded'
+ )
+ self.assertEqual(
+ self.get(url='/%20%64%69%20%72%20%2F%66%69%20%6C%65')['body'],
+ 'blah',
+ 'encoded 2',
+ )
+
+ os.rename(
+ self.testdir + '/assets/ di r /fi le',
+ self.testdir + '/assets/ di r / fi le ',
+ )
+ self.waitforfiles(self.testdir + '/assets/ di r / fi le ')
+ self.assertEqual(
+ self.get(url='/%20di%20r%20/%20fi%20le%20')['body'],
+ 'blah',
+ 'file name enclosing',
+ )
+
+ try:
+ print('файл')
+ utf8 = True
+
+ except:
+ utf8 = False
+
+ if utf8:
+ os.rename(
+ self.testdir + '/assets/ di r / fi le ',
+ self.testdir + '/assets/ di r /фа йл',
+ )
+ self.waitforfiles(self.testdir + '/assets/ di r /фа йл')
+ self.assertEqual(
+ self.get(url='/ di r /фа йл')['body'], 'blah', 'file name 2'
+ )
+
+ os.rename(
+ self.testdir + '/assets/ di r ',
+ self.testdir + '/assets/ди ректория',
+ )
+ self.waitforfiles(self.testdir + '/assets/ди ректория/фа йл')
+ self.assertEqual(
+ self.get(url='/ди ректория/фа йл')['body'], 'blah', 'dir name 2'
+ )
+
+ def test_static_head(self):
+ resp = self.head(url='/')
+ self.assertEqual(resp['status'], 200, 'status')
+ self.assertEqual(resp['body'], '', 'empty body')
+
+ def test_static_two_clients(self):
+ _, sock = self.get(url='/', start=True, no_recv=True)
+ _, sock2 = self.get(url='/', start=True, no_recv=True)
+
+ self.assertEqual(sock.recv(1), b'H', 'client 1')
+ self.assertEqual(sock2.recv(1), b'H', 'client 2')
+ self.assertEqual(sock.recv(1), b'T', 'client 1 again')
+ self.assertEqual(sock2.recv(1), b'T', 'client 2 again')
+
+ sock.close()
+ sock2.close()
+
+ def test_static_mime_types(self):
+ self.assertIn(
+ 'success',
+ self.conf(
+ {
+ "text/x-code/x-blah/x-blah": "readme",
+ "text/plain": [".html", ".log", "file"],
+ },
+ 'settings/http/static/mime_types',
+ ),
+ 'configure mime_types',
+ )
+
+ self.assertEqual(
+ self.get(url='/README')['headers']['Content-Type'],
+ 'text/x-code/x-blah/x-blah',
+ 'mime_types string case insensitive',
+ )
+ self.assertEqual(
+ self.get(url='/index.html')['headers']['Content-Type'],
+ 'text/plain',
+ 'mime_types html',
+ )
+ self.assertEqual(
+ self.get(url='/')['headers']['Content-Type'],
+ 'text/plain',
+ 'mime_types index default',
+ )
+ self.assertEqual(
+ self.get(url='/dir/file')['headers']['Content-Type'],
+ 'text/plain',
+ 'mime_types file in dir',
+ )
+
+ def test_static_mime_types_partial_match(self):
+ self.assertIn(
+ 'success',
+ self.conf(
+ {
+ "text/x-blah": ["ile", "fil", "f", "e", ".file"],
+ },
+ 'settings/http/static/mime_types',
+ ),
+ 'configure mime_types',
+ )
+ self.assertNotIn(
+ 'Content-Type', self.get(url='/dir/file'), 'partial match'
+ )
+
+ def test_static_mime_types_reconfigure(self):
+ self.assertIn(
+ 'success',
+ self.conf(
+ {
+ "text/x-code": "readme",
+ "text/plain": [".html", ".log", "file"],
+ },
+ 'settings/http/static/mime_types',
+ ),
+ 'configure mime_types',
+ )
+
+ self.assertEqual(
+ self.conf_get('settings/http/static/mime_types'),
+ {'text/x-code': 'readme', 'text/plain': ['.html', '.log', 'file']},
+ 'mime_types get',
+ )
+ self.assertEqual(
+ self.conf_get('settings/http/static/mime_types/text%2Fx-code'),
+ 'readme',
+ 'mime_types get string',
+ )
+ self.assertEqual(
+ self.conf_get('settings/http/static/mime_types/text%2Fplain'),
+ ['.html', '.log', 'file'],
+ 'mime_types get array',
+ )
+ self.assertEqual(
+ self.conf_get('settings/http/static/mime_types/text%2Fplain/1'),
+ '.log',
+ 'mime_types get array element',
+ )
+
+ self.assertIn(
+ 'success',
+ self.conf_delete('settings/http/static/mime_types/text%2Fplain/2'),
+ 'mime_types remove array element',
+ )
+ self.assertNotIn(
+ 'Content-Type',
+ self.get(url='/dir/file')['headers'],
+ 'mime_types removed',
+ )
+
+ self.assertIn(
+ 'success',
+ self.conf_post(
+ '"file"', 'settings/http/static/mime_types/text%2Fplain'
+ ),
+ 'mime_types add array element',
+ )
+ self.assertEqual(
+ self.get(url='/dir/file')['headers']['Content-Type'],
+ 'text/plain',
+ 'mime_types reverted',
+ )
+
+ self.assertIn(
+ 'success',
+ self.conf(
+ '"file"', 'settings/http/static/mime_types/text%2Fplain'
+ ),
+ 'configure mime_types update',
+ )
+ self.assertEqual(
+ self.get(url='/dir/file')['headers']['Content-Type'],
+ 'text/plain',
+ 'mime_types updated',
+ )
+ self.assertNotIn(
+ 'Content-Type',
+ self.get(url='/log.log')['headers'],
+ 'mime_types updated 2',
+ )
+
+ self.assertIn(
+ 'success',
+ self.conf(
+ '".log"', 'settings/http/static/mime_types/text%2Fblahblahblah'
+ ),
+ 'configure mime_types create',
+ )
+ self.assertEqual(
+ self.get(url='/log.log')['headers']['Content-Type'],
+ 'text/blahblahblah',
+ 'mime_types create',
+ )
+
+ def test_static_mime_types_correct(self):
+ self.assertIn(
+ 'error',
+ self.conf(
+ {"text/x-code": "readme", "text/plain": "readme"},
+ 'settings/http/static/mime_types',
+ ),
+ 'mime_types same extensions',
+ )
+ self.assertIn(
+ 'error',
+ self.conf(
+ {"text/x-code": [".h", ".c"], "text/plain": ".c"},
+ 'settings/http/static/mime_types',
+ ),
+ 'mime_types same extensions array',
+ )
+ self.assertIn(
+ 'error',
+ self.conf(
+ {
+ "text/x-code": [".h", ".c", "readme"],
+ "text/plain": "README",
+ },
+ 'settings/http/static/mime_types',
+ ),
+ 'mime_types same extensions case insensitive',
+ )
+
+ @unittest.skip('not yet')
+ def test_static_mime_types_invalid(self):
+ self.assertIn(
+ 'error',
+ self.http(
+ b"""PUT /config/settings/http/static/mime_types/%0%00% HTTP/1.1\r
+Host: localhost\r
+Connection: close\r
+Content-Length: 6\r
+\r
+\"blah\"""",
+ raw_resp=True,
+ raw=True,
+ sock_type='unix',
+ addr=self.testdir + '/control.unit.sock',
+ ),
+ 'mime_types invalid',
+ )
+
+if __name__ == '__main__':
+ TestStatic.main()
diff --git a/test/test_tls.py b/test/test_tls.py
index 076a2c38..3514bbcb 100644
--- a/test/test_tls.py
+++ b/test/test_tls.py
@@ -6,7 +6,7 @@ from unit.applications.tls import TestApplicationTLS
class TestTLS(TestApplicationTLS):
- prerequisites = ['python', 'openssl']
+ prerequisites = {'modules': ['python', 'openssl']}
def findall(self, pattern):
with open(self.testdir + '/unit.log', 'r', errors='ignore') as f:
diff --git a/test/unit/applications/lang/go.py b/test/unit/applications/lang/go.py
index e4ab8ffa..15ac1cd9 100644
--- a/test/unit/applications/lang/go.py
+++ b/test/unit/applications/lang/go.py
@@ -4,12 +4,22 @@ from unit.applications.proto import TestApplicationProto
class TestApplicationGo(TestApplicationProto):
- def load(self, script, name='app'):
+ @classmethod
+ def setUpClass(cls, complete_check=True):
+ unit = super().setUpClass(complete_check=False)
- if not os.path.isdir(self.testdir + '/go'):
- os.mkdir(self.testdir + '/go')
+ # check go module
+
+ go_app = TestApplicationGo()
+ go_app.testdir = unit.testdir
+ if go_app.prepare_env('empty', 'app').returncode == 0:
+ cls.available['modules']['go'] = []
- go_app_path = self.current_dir + '/go/'
+ return unit if not complete_check else unit.complete()
+
+ def prepare_env(self, script, name):
+ if not os.path.exists(self.testdir + '/go'):
+ os.mkdir(self.testdir + '/go')
env = os.environ.copy()
env['GOPATH'] = self.pardir + '/go'
@@ -19,12 +29,18 @@ class TestApplicationGo(TestApplicationProto):
'build',
'-o',
self.testdir + '/go/' + name,
- go_app_path + script + '/' + name + '.go',
+ self.current_dir + '/go/' + script + '/' + name + '.go',
],
env=env,
)
+
process.communicate()
+ return process
+
+ def load(self, script, name='app'):
+ self.prepare_env(script, name)
+
self._load_conf(
{
"listeners": {"*:7080": {"pass": "applications/" + script}},
@@ -32,8 +48,10 @@ class TestApplicationGo(TestApplicationProto):
script: {
"type": "external",
"processes": {"spare": 0},
- "working_directory": go_app_path + script,
- "executable": self.testdir + '/go/' + name,
+ "working_directory": self.current_dir
+ + "/go/"
+ + script,
+ "executable": self.testdir + "/go/" + name,
}
},
}
diff --git a/test/unit/applications/lang/java.py b/test/unit/applications/lang/java.py
index ec1c95d9..40bf3662 100644
--- a/test/unit/applications/lang/java.py
+++ b/test/unit/applications/lang/java.py
@@ -1,4 +1,5 @@
import os
+import glob
import shutil
from subprocess import Popen
from unit.applications.proto import TestApplicationProto
@@ -6,11 +7,9 @@ from unit.applications.proto import TestApplicationProto
class TestApplicationJava(TestApplicationProto):
def load(self, script, name='app'):
-
app_path = self.testdir + '/java'
web_inf_path = app_path + '/WEB-INF/'
classes_path = web_inf_path + 'classes/'
-
script_path = self.current_dir + '/java/' + script + '/'
if not os.path.isdir(app_path):
@@ -19,39 +18,48 @@ class TestApplicationJava(TestApplicationProto):
src = []
for f in os.listdir(script_path):
+ file_path = script_path + f
+
if f.endswith('.java'):
- src.append(script_path + f)
+ src.append(file_path)
continue
if f.startswith('.') or f == 'Makefile':
continue
- if os.path.isdir(script_path + f):
+ if os.path.isdir(file_path):
if f == 'WEB-INF':
continue
- shutil.copytree(script_path + f, app_path + '/' + f)
+ shutil.copytree(file_path, app_path + '/' + f)
continue
if f == 'web.xml':
if not os.path.isdir(web_inf_path):
os.makedirs(web_inf_path)
- shutil.copy2(script_path + f, web_inf_path)
+ shutil.copy2(file_path, web_inf_path)
else:
- shutil.copy2(script_path + f, app_path)
+ shutil.copy2(file_path, app_path)
if src:
if not os.path.isdir(classes_path):
os.makedirs(classes_path)
- tomcat_jar = self.pardir + '/build/tomcat-servlet-api-9.0.13.jar'
+ classpath = self.pardir + '/build/tomcat-servlet-api-9.0.13.jar'
+
+ ws_jars = glob.glob(
+ self.pardir + '/build/websocket-api-java-*.jar'
+ )
+
+ if not ws_jars:
+ self.fail('websocket api jar not found.')
javac = [
'javac',
'-encoding', 'utf-8',
'-d', classes_path,
- '-classpath', tomcat_jar,
+ '-classpath', classpath + ':' + ws_jars[0],
]
javac.extend(src)
diff --git a/test/unit/applications/lang/node.py b/test/unit/applications/lang/node.py
index 931c6596..3cc72669 100644
--- a/test/unit/applications/lang/node.py
+++ b/test/unit/applications/lang/node.py
@@ -4,8 +4,18 @@ from unit.applications.proto import TestApplicationProto
class TestApplicationNode(TestApplicationProto):
- def load(self, script, name='app.js'):
+ @classmethod
+ def setUpClass(cls, complete_check=True):
+ unit = super().setUpClass(complete_check=False)
+
+ # check node module
+
+ if os.path.exists(unit.pardir + '/node/node_modules'):
+ cls.available['modules']['node'] = []
+ return unit if not complete_check else unit.complete()
+
+ def load(self, script, name='app.js'):
# copy application
shutil.copytree(
diff --git a/test/unit/applications/tls.py b/test/unit/applications/tls.py
index 6e8deefb..1290279d 100644
--- a/test/unit/applications/tls.py
+++ b/test/unit/applications/tls.py
@@ -1,4 +1,5 @@
import os
+import re
import ssl
import subprocess
from unit.applications.proto import TestApplicationProto
@@ -12,6 +13,27 @@ class TestApplicationTLS(TestApplicationProto):
self.context.check_hostname = False
self.context.verify_mode = ssl.CERT_NONE
+ @classmethod
+ def setUpClass(cls, complete_check=True):
+ unit = super().setUpClass(complete_check=False)
+
+ # check tls module
+
+ try:
+ subprocess.check_output(['which', 'openssl'])
+
+ output = subprocess.check_output(
+ [unit.unitd, '--version'], stderr=subprocess.STDOUT
+ )
+
+ if re.search('--openssl', output.decode()):
+ cls.available['modules']['openssl'] = []
+
+ except:
+ pass
+
+ return unit if not complete_check else unit.complete()
+
def certificate(self, name='default', load=True):
self.openssl_conf()
diff --git a/test/unit/applications/websockets.py b/test/unit/applications/websockets.py
index 417e9504..50ff2797 100644
--- a/test/unit/applications/websockets.py
+++ b/test/unit/applications/websockets.py
@@ -54,24 +54,16 @@ class TestApplicationWebsocket(TestApplicationProto):
def apply_mask(self, data, mask):
return bytes(b ^ m for b, m in zip(data, itertools.cycle(mask)))
- def serialize_close(self, code = 1000, reason = ''):
+ def serialize_close(self, code=1000, reason=''):
return struct.pack('!H', code) + reason.encode('utf-8')
- def frame_read(self, sock, read_timeout=10):
+ def frame_read(self, sock, read_timeout=30):
def recv_bytes(sock, bytes):
data = b''
while select.select([sock], [], [], read_timeout)[0]:
- try:
- if bytes < 65536:
- data = sock.recv(bytes)
- else:
- data = self.recvall(
- sock,
- read_timeout=read_timeout,
- buff_size=bytes,
- )
- break
- except:
+ data += sock.recv(bytes - len(data))
+
+ if len(data) == bytes:
break
return data
@@ -99,7 +91,11 @@ class TestApplicationWebsocket(TestApplicationProto):
if frame['mask']:
mask_bits = recv_bytes(sock, 4)
- data = recv_bytes(sock, length)
+ data = b''
+
+ if length != 0:
+ data = recv_bytes(sock, length)
+
if frame['mask']:
data = self.apply_mask(data, mask_bits)
@@ -175,14 +171,20 @@ class TestApplicationWebsocket(TestApplicationProto):
frame = self.frame_to_send(*args, **kwargs)
if chopsize is None:
- sock.sendall(frame)
+ try:
+ sock.sendall(frame)
+ except BrokenPipeError:
+ pass
else:
pos = 0
frame_len = len(frame)
- while (pos < frame_len):
+ while pos < frame_len:
end = min(pos + chopsize, frame_len)
- sock.sendall(frame[pos:end])
+ try:
+ sock.sendall(frame[pos:end])
+ except BrokenPipeError:
+ end = frame_len
pos = end
def message(self, sock, type, message, fragmention_size=None, **kwargs):
@@ -197,17 +199,19 @@ class TestApplicationWebsocket(TestApplicationProto):
pos = 0
op_code = type
- while(pos < message_len):
+ while pos < message_len:
end = min(pos + fragmention_size, message_len)
- fin = (end == message_len)
- self.frame_write(sock, op_code, message[pos:end], fin=fin, **kwargs)
+ fin = end == message_len
+ self.frame_write(
+ sock, op_code, message[pos:end], fin=fin, **kwargs
+ )
op_code = self.OP_CONT
pos = end
def message_read(self, sock, read_timeout=10):
frame = self.frame_read(sock, read_timeout=read_timeout)
- while(not frame['fin']):
+ while not frame['fin']:
temp = self.frame_read(sock, read_timeout=read_timeout)
frame['data'] += temp['data']
frame['fin'] = temp['fin']
diff --git a/test/unit/feature/isolation.py b/test/unit/feature/isolation.py
new file mode 100644
index 00000000..9b06ab3c
--- /dev/null
+++ b/test/unit/feature/isolation.py
@@ -0,0 +1,87 @@
+import os
+import json
+from unit.applications.proto import TestApplicationProto
+from unit.applications.lang.go import TestApplicationGo
+from unit.applications.lang.java import TestApplicationJava
+from unit.applications.lang.node import TestApplicationNode
+from unit.applications.lang.perl import TestApplicationPerl
+from unit.applications.lang.php import TestApplicationPHP
+from unit.applications.lang.python import TestApplicationPython
+from unit.applications.lang.ruby import TestApplicationRuby
+
+
+class TestFeatureIsolation(TestApplicationProto):
+ allns = ['pid', 'mnt', 'ipc', 'uts', 'cgroup', 'net']
+
+ def check(self, available, testdir):
+ test_conf = {"namespaces": {"credential": True}}
+
+ module = ''
+ app = 'empty'
+ if 'go' in available['modules']:
+ module = TestApplicationGo()
+
+ elif 'java' in available['modules']:
+ module = TestApplicationJava()
+
+ elif 'node' in available['modules']:
+ module = TestApplicationNode()
+ app = 'basic'
+
+ elif 'perl' in available['modules']:
+ module = TestApplicationPerl()
+ app = 'body_empty'
+
+ elif 'php' in available['modules']:
+ module = TestApplicationPHP()
+ app = 'phpinfo'
+
+ elif 'python' in available['modules']:
+ module = TestApplicationPython()
+
+ elif 'ruby' in available['modules']:
+ module = TestApplicationRuby()
+
+ if not module:
+ return
+
+ module.testdir = testdir
+ module.load(app)
+
+ resp = module.conf(test_conf, 'applications/' + app + '/isolation')
+ if 'success' not in resp:
+ return
+
+ userns = self.getns('user')
+ if not userns:
+ return
+
+ available['features']['isolation'] = {'user': userns}
+
+ unp_clone_path = '/proc/sys/kernel/unprivileged_userns_clone'
+ if os.path.exists(unp_clone_path):
+ with open(unp_clone_path, 'r') as f:
+ if str(f.read()).rstrip() == '1':
+ available['features']['isolation'][
+ 'unprivileged_userns_clone'
+ ] = True
+
+ for ns in self.allns:
+ ns_value = self.getns(ns)
+ if ns_value:
+ available['features']['isolation'][ns] = ns_value
+
+ def getns(self, nstype):
+ # read namespace id from symlink file:
+ # it points to: '<nstype>:[<ns id>]'
+ # # eg.: 'pid:[4026531836]'
+ nspath = '/proc/self/ns/' + nstype
+ data = None
+
+ if os.path.exists(nspath):
+ data = int(os.readlink(nspath)[len(nstype) + 2 : -1])
+
+ return data
+
+ def parsejson(self, data):
+ return json.loads(data.split('\n')[1])
diff --git a/test/unit/http.py b/test/unit/http.py
index c0af8a9e..82a6bd6a 100644
--- a/test/unit/http.py
+++ b/test/unit/http.py
@@ -12,6 +12,11 @@ class TestHTTP(TestUnit):
port = 7080 if 'port' not in kwargs else kwargs['port']
url = '/' if 'url' not in kwargs else kwargs['url']
http = 'HTTP/1.0' if 'http_10' in kwargs else 'HTTP/1.1'
+ read_buffer_size = (
+ 4096
+ if 'read_buffer_size' not in kwargs
+ else kwargs['read_buffer_size']
+ )
headers = (
{'Host': 'localhost', 'Connection': 'close'}
@@ -94,7 +99,9 @@ class TestHTTP(TestUnit):
read_timeout = (
30 if 'read_timeout' not in kwargs else kwargs['read_timeout']
)
- resp = self.recvall(sock, read_timeout=read_timeout).decode(enc)
+ resp = self.recvall(
+ sock, read_timeout=read_timeout, buff_size=read_buffer_size
+ ).decode(enc)
if TestUnit.detailed:
print('<<<')
@@ -118,6 +125,9 @@ class TestHTTP(TestUnit):
def get(self, **kwargs):
return self.http('GET', **kwargs)
+ def head(self, **kwargs):
+ return self.http('HEAD', **kwargs)
+
def post(self, **kwargs):
return self.http('POST', **kwargs)
diff --git a/test/unit/main.py b/test/unit/main.py
index 6a167a9e..873f1815 100644
--- a/test/unit/main.py
+++ b/test/unit/main.py
@@ -12,8 +12,6 @@ import subprocess
from multiprocessing import Process
-available_modules = {}
-
class TestUnit(unittest.TestCase):
current_dir = os.path.abspath(
@@ -28,6 +26,7 @@ class TestUnit(unittest.TestCase):
detailed = False
save_log = False
+ unsafe = False
def __init__(self, methodName='runTest'):
super().__init__(methodName)
@@ -41,10 +40,12 @@ class TestUnit(unittest.TestCase):
if not hasattr(self, 'application_type'):
return super().run(result)
+ # rerun test for each available module version
+
type = self.application_type
- for prerequisite in self.prerequisites:
- if prerequisite in available_modules:
- for version in available_modules[prerequisite]:
+ for module in self.prerequisites['modules']:
+ if module in self.available['modules']:
+ for version in self.available['modules'][module]:
self.application_type = type + ' ' + version
super().run(result)
@@ -63,8 +64,83 @@ class TestUnit(unittest.TestCase):
unittest.main()
@classmethod
- def setUpClass(cls):
- TestUnit().check_modules(*cls.prerequisites)
+ def setUpClass(cls, complete_check=True):
+ cls.available = {'modules': {}, 'features': {}}
+ unit = TestUnit()
+
+ unit._run()
+
+ # read unit.log
+
+ for i in range(50):
+ with open(unit.testdir + '/unit.log', 'r') as f:
+ log = f.read()
+ m = re.search('controller started', log)
+
+ if m is None:
+ time.sleep(0.1)
+ else:
+ break
+
+ if m is None:
+ unit.stop()
+ exit("Unit is writing log too long")
+
+ # discover available modules from unit.log
+
+ for module in re.findall(r'module: ([a-zA-Z]+) (.*) ".*"$', log, re.M):
+ if module[0] not in cls.available['modules']:
+ cls.available['modules'][module[0]] = [module[1]]
+ else:
+ cls.available['modules'][module[0]].append(module[1])
+
+ def check(available, prerequisites):
+ missed = []
+
+ # check modules
+
+ if 'modules' in prerequisites:
+ available_modules = list(available['modules'].keys())
+
+ for module in prerequisites['modules']:
+ if module in available_modules:
+ continue
+
+ missed.append(module)
+
+ if missed:
+ print('Unit has no ' + ', '.join(missed) + ' module(s)')
+ raise unittest.SkipTest()
+
+ # check features
+
+ if 'features' in prerequisites:
+ available_features = list(available['features'].keys())
+
+ for feature in prerequisites['features']:
+ if feature in available_features:
+ continue
+
+ missed.append(feature)
+
+ if missed:
+ print(', '.join(missed) + ' feature(s) not supported')
+ raise unittest.SkipTest()
+
+ def destroy():
+ unit.stop()
+ unit._check_alerts(log)
+ shutil.rmtree(unit.testdir)
+
+ def complete():
+ destroy()
+ check(cls.available, cls.prerequisites)
+
+ if complete_check:
+ complete()
+ else:
+ unit.complete = complete
+ return unit
def setUp(self):
self._run()
@@ -105,92 +181,6 @@ class TestUnit(unittest.TestCase):
else:
self._print_path_to_log()
- def check_modules(self, *modules):
- self._run()
-
- for i in range(50):
- with open(self.testdir + '/unit.log', 'r') as f:
- log = f.read()
- m = re.search('controller started', log)
-
- if m is None:
- time.sleep(0.1)
- else:
- break
-
- if m is None:
- self.stop()
- exit("Unit is writing log too long")
-
- # discover all available modules
-
- global available_modules
- available_modules = {}
- for module in re.findall(r'module: ([a-zA-Z]+) (.*) ".*"$', log, re.M):
- if module[0] not in available_modules:
- available_modules[module[0]] = [module[1]]
- else:
- available_modules[module[0]].append(module[1])
-
- missed_module = ''
- for module in modules:
- if module == 'go':
- env = os.environ.copy()
- env['GOPATH'] = self.pardir + '/go'
-
- try:
- process = subprocess.Popen(
- [
- 'go',
- 'build',
- '-o',
- self.testdir + '/go/check_module',
- self.current_dir + '/go/empty/app.go',
- ],
- env=env,
- )
- process.communicate()
-
- m = module if process.returncode == 0 else None
-
- except:
- m = None
-
- elif module == 'node':
- if os.path.isdir(self.pardir + '/node/node_modules'):
- m = module
- else:
- m = None
-
- elif module == 'openssl':
- try:
- subprocess.check_output(['which', 'openssl'])
-
- output = subprocess.check_output(
- [self.unitd, '--version'],
- stderr=subprocess.STDOUT,
- )
-
- m = re.search('--openssl', output.decode())
-
- except:
- m = None
-
- else:
- if module not in available_modules:
- m = None
-
- if m is None:
- missed_module = module
- break
-
- self.stop()
- self._check_alerts(log)
- shutil.rmtree(self.testdir)
-
- if missed_module:
- raise unittest.SkipTest('Unit has no ' + missed_module + ' module')
-
def stop(self):
if self._started:
self._stop()
@@ -350,6 +340,8 @@ class TestUnit(unittest.TestCase):
TestUnit.save_log = args.save_log
TestUnit.unsafe = args.unsafe
+ # set stdout to non-blocking
+
if TestUnit.detailed:
fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, 0)
diff --git a/version b/version
index 271e354a..427db61b 100644
--- a/version
+++ b/version
@@ -1,5 +1,5 @@
# Copyright (C) NGINX, Inc.
-NXT_VERSION=1.10.0
-NXT_VERNUM=11000
+NXT_VERSION=1.11.0
+NXT_VERNUM=11100