summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/tools/pywebsocket
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /testing/web-platform/tests/tools/pywebsocket
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'testing/web-platform/tests/tools/pywebsocket')
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/COPYING28
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/MANIFEST.in6
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/README17
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/abort_handshake_wsh.py43
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/abort_wsh.py43
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/arraybuffer_benchmark.html134
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/bench_wsh.py60
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/benchmark.html203
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/benchmark.js309
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/benchmark_helper_wsh.py85
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/close_wsh.py69
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/console.html317
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/cookie_wsh.py32
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/example/echo_client.py1128
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/echo_noext_wsh.py61
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/echo_wsh.py54
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/example/eventsource.cgi54
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/eventsource.html74
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/handler_map.txt11
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/hsts_wsh.py40
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/internal_error_wsh.py42
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/origin_check_wsh.py44
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/pywebsocket.conf42
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/example/special_headers.cgi28
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/util.js177
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/util_main.js63
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/util_worker.js19
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/xhr_benchmark.html222
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/xhr_benchmark.js389
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/example/xhr_event_logger.html110
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/__init__.py224
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_base.py181
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_hixie75.py229
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_hybi.py887
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/common.py303
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/dispatch.py393
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/extensions.py885
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/fast_masking.i98
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/__init__.py110
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/_base.py182
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/hybi.py420
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/hybi00.py293
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/headerparserhandler.py254
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/http_header_util.py263
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/memorizingfile.py99
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/msgutil.py219
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/mux.py1889
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/standalone.py1193
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/stream.py57
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/util.py416
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/xhr_benchmark_handler.py109
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/setup.py74
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/cert/cacert.pem17
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/cert/cert.pem61
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/cert/client_cert.p12bin0 -> 2582 bytes
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/cert/key.pem15
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/client_for_testing.py1100
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/endtoend_with_external_server.py67
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/mock.py221
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/mux_client_for_testing.py690
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/run_all.py89
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/set_sys_path.py45
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_dispatch.py288
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_endtoend.py753
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_extensions.py360
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_handshake.py188
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi.py534
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi00.py516
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_http_header_util.py90
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_memorizingfile.py104
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_mock.py145
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_msgutil.py1356
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/test_mux.py2089
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_stream.py77
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_stream_hixie75.py59
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_util.py200
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/README1
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/abort_by_user_wsh.py42
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/blank_wsh.py31
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/origin_check_wsh.py42
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/exception_in_transfer_wsh.py44
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/no_wsh_at_the_end.py45
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/non_callable_wsh.py39
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/plain_wsh.py40
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py45
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py45
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/hello.pl32
88 files changed, 22152 insertions, 0 deletions
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/COPYING b/testing/web-platform/tests/tools/pywebsocket/src/COPYING
new file mode 100644
index 000000000..989d02e4c
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/COPYING
@@ -0,0 +1,28 @@
+Copyright 2012, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/MANIFEST.in b/testing/web-platform/tests/tools/pywebsocket/src/MANIFEST.in
new file mode 100644
index 000000000..19256882c
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/MANIFEST.in
@@ -0,0 +1,6 @@
+include COPYING
+include MANIFEST.in
+include README
+recursive-include example *.py
+recursive-include mod_pywebsocket *.py
+recursive-include test *.py
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/README b/testing/web-platform/tests/tools/pywebsocket/src/README
new file mode 100644
index 000000000..c8c758f5e
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/README
@@ -0,0 +1,17 @@
+INSTALL
+
+To install this package to the system, run this:
+$ python setup.py build
+$ sudo python setup.py install
+
+To install this package as a normal user, run this instead:
+$ python setup.py build
+$ python setup.py install --user
+
+LAUNCH
+
+To use pywebsocket as Apache module, run this to read the document:
+$ pydoc mod_pywebsocket
+
+To use pywebsocket as standalone server, run this to read the document:
+$ pydoc mod_pywebsocket.standalone
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/abort_handshake_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/abort_handshake_wsh.py
new file mode 100644
index 000000000..008023a1f
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/abort_handshake_wsh.py
@@ -0,0 +1,43 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+from mod_pywebsocket import handshake
+
+
+def web_socket_do_extra_handshake(request):
+ raise handshake.AbortedByUserException(
+ "Aborted in web_socket_do_extra_handshake")
+
+
+def web_socket_transfer_data(request):
+ pass
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/abort_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/abort_wsh.py
new file mode 100644
index 000000000..2bbf005f6
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/abort_wsh.py
@@ -0,0 +1,43 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+from mod_pywebsocket import handshake
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ raise handshake.AbortedByUserException(
+ "Aborted in web_socket_transfer_data")
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/arraybuffer_benchmark.html b/testing/web-platform/tests/tools/pywebsocket/src/example/arraybuffer_benchmark.html
new file mode 100644
index 000000000..869cd7e1e
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/arraybuffer_benchmark.html
@@ -0,0 +1,134 @@
+<!--
+Copyright 2013, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+
+<html>
+<head>
+<title>ArrayBuffer benchmark</title>
+<script src="util.js"></script>
+<script>
+var PRINT_SIZE = true;
+
+// Initial size of arrays.
+var START_SIZE = 10 * 1024;
+// Stops benchmark when the size of an array exceeds this threshold.
+var STOP_THRESHOLD = 100000 * 1024;
+// If the size of each array is small, write/read the array multiple times
+// until the sum of sizes reaches this threshold.
+var MIN_TOTAL = 100000 * 1024;
+var MULTIPLIERS = [5, 2];
+
+// Repeat benchmark for several times to measure performance of optimized
+// (such as JIT) run.
+var REPEAT_FOR_WARMUP = 3;
+
+function writeBenchmark(size, minTotal) {
+ var totalSize = 0;
+ while (totalSize < minTotal) {
+ var arrayBuffer = new ArrayBuffer(size);
+
+ // Write 'a's.
+ fillArrayBuffer(arrayBuffer, 0x61);
+
+ totalSize += size;
+ }
+ return totalSize;
+}
+
+function readBenchmark(size, minTotal) {
+ var totalSize = 0;
+ while (totalSize < minTotal) {
+ var arrayBuffer = new ArrayBuffer(size);
+
+ if (!verifyArrayBuffer(arrayBuffer, 0x00)) {
+ queueLog('Verification failed');
+ return -1;
+ }
+
+ totalSize += size;
+ }
+ return totalSize;
+}
+
+function runBenchmark(benchmarkFunction,
+ size,
+ stopThreshold,
+ minTotal,
+ multipliers,
+ multiplierIndex) {
+ while (size <= stopThreshold) {
+ var maxSpeed = 0;
+
+ for (var i = 0; i < REPEAT_FOR_WARMUP; ++i) {
+ var startTimeInMs = getTimeStamp();
+
+ var totalSize = benchmarkFunction(size, minTotal);
+
+ maxSpeed = Math.max(maxSpeed,
+ calculateSpeedInKB(totalSize, startTimeInMs));
+ }
+ queueLog(formatResultInKiB(size, maxSpeed, PRINT_SIZE));
+
+ size *= multipliers[multiplierIndex];
+ multiplierIndex = (multiplierIndex + 1) % multipliers.length;
+ }
+}
+
+function runBenchmarks() {
+ queueLog('Message size in KiB, Speed in kB/s');
+
+ queueLog('Write benchmark');
+ runBenchmark(
+ writeBenchmark, START_SIZE, STOP_THRESHOLD, MIN_TOTAL, MULTIPLIERS, 0);
+ queueLog('Finished');
+
+ queueLog('Read benchmark');
+ runBenchmark(
+ readBenchmark, START_SIZE, STOP_THRESHOLD, MIN_TOTAL, MULTIPLIERS, 0);
+ addToLog('Finished');
+}
+
+function init() {
+ logBox = document.getElementById('log');
+
+ queueLog(window.navigator.userAgent.toLowerCase());
+
+ addToLog('Started...');
+
+ setTimeout(runBenchmarks, 0);
+}
+
+</script>
+</head>
+<body onload="init()">
+<textarea
+ id="log" rows="50" style="width: 100%" readonly></textarea>
+</body>
+</html>
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/bench_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/bench_wsh.py
new file mode 100644
index 000000000..5067ca7d8
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/bench_wsh.py
@@ -0,0 +1,60 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""A simple load tester for WebSocket clients.
+
+A client program sends a message formatted as "<time> <count> <message>" to
+this handler. This handler starts sending total <count> WebSocket messages
+containing <message> every <time> seconds. <time> can be a floating point
+value. <count> must be an integer value.
+"""
+
+
+import time
+
+
+def web_socket_do_extra_handshake(request):
+ pass # Always accept.
+
+
+def web_socket_transfer_data(request):
+ line = request.ws_stream.receive_message()
+ parts = line.split(' ')
+ if len(parts) != 3:
+ raise ValueError('Bad parameter format')
+ wait = float(parts[0])
+ count = int(parts[1])
+ message = parts[2]
+ for i in xrange(count):
+ request.ws_stream.send_message(message)
+ time.sleep(wait)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark.html b/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark.html
new file mode 100644
index 000000000..3a218173a
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark.html
@@ -0,0 +1,203 @@
+<!--
+Copyright 2013, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+
+<html>
+<head>
+<title>WebSocket benchmark</title>
+<script src="util_main.js"></script>
+<script src="util.js"></script>
+<script src="benchmark.js"></script>
+<script>
+var addressBox = null;
+
+function getConfig() {
+ return {
+ prefixUrl: addressBox.value,
+ printSize: getBoolFromCheckBox('printsize'),
+ numSockets: getIntFromInput('numsockets'),
+ // Initial size of messages.
+ numIterations: getIntFromInput('numiterations'),
+ numWarmUpIterations: getIntFromInput('numwarmupiterations'),
+ startSize: getIntFromInput('startsize'),
+ // Stops benchmark when the size of message exceeds this threshold.
+ stopThreshold: getIntFromInput('stopthreshold'),
+ // If the size of each message is small, send/receive multiple messages
+ // until the sum of sizes reaches this threshold.
+ minTotal: getIntFromInput('mintotal'),
+ multipliers: getIntArrayFromInput('multipliers'),
+ verifyData: getBoolFromCheckBox('verifydata')
+ };
+}
+
+var worker = new Worker('benchmark.js');
+worker.onmessage = onMessage;
+
+function onSendBenchmark() {
+ var config = getConfig();
+
+ if (getBoolFromCheckBox('worker')) {
+ worker.postMessage({type: 'sendBenchmark', config: config});
+ } else {
+ config.addToLog = addToLog;
+ config.addToSummary = addToSummary;
+ config.measureValue = measureValue;
+ sendBenchmark(config);
+ }
+}
+
+function onReceiveBenchmark() {
+ var config = getConfig();
+
+ if (getBoolFromCheckBox('worker')) {
+ worker.postMessage({type: 'receiveBenchmark', config: config});
+ } else {
+ config.addToLog = addToLog;
+ config.addToSummary = addToSummary;
+ config.measureValue = measureValue;
+ receiveBenchmark(config);
+ }
+}
+
+function onBatchBenchmark() {
+ var config = getConfig();
+
+ if (getBoolFromCheckBox('worker')) {
+ worker.postMessage({type: 'batchBenchmark', config: config});
+ } else {
+ config.addToLog = addToLog;
+ config.addToSummary = addToSummary;
+ config.measureValue = measureValue;
+ batchBenchmark(config);
+ }
+}
+
+function onStop() {
+ var config = getConfig();
+
+ if (getBoolFromCheckBox('worker')) {
+ worker.postMessage({type: 'stop', config: config});
+ } else {
+ config.addToLog = addToLog;
+ config.addToSummary = addToSummary;
+ config.measureValue = measureValue;
+ stop(config);
+ }
+}
+function init() {
+ addressBox = document.getElementById('address');
+ logBox = document.getElementById('log');
+
+ summaryBox = document.getElementById('summary');
+
+ var scheme = window.location.protocol == 'https:' ? 'wss://' : 'ws://';
+ var defaultAddress = scheme + window.location.host + '/benchmark_helper';
+
+ addressBox.value = defaultAddress;
+
+ addToLog(window.navigator.userAgent.toLowerCase());
+ addToSummary(window.navigator.userAgent.toLowerCase());
+
+ if (!('WebSocket' in window)) {
+ addToLog('WebSocket is not available');
+ }
+}
+</script>
+</head>
+<body onload="init()">
+
+<div id="benchmark_div">
+ url <input type="text" id="address" size="40">
+ <input type="button" value="send" onclick="onSendBenchmark()">
+ <input type="button" value="receive" onclick="onReceiveBenchmark()">
+ <input type="button" value="batch" onclick="onBatchBenchmark()">
+ <input type="button" value="stop" onclick="onStop()">
+
+ <br/>
+
+ <input type="checkbox" id="printsize" checked>
+ <label for="printsize">Print size and time per message</label>
+ <input type="checkbox" id="verifydata" checked>
+ <label for="verifydata">Verify data</label>
+ <input type="checkbox" id="worker">
+ <label for="worker">Run on worker</label>
+
+ <br/>
+
+ Parameters:
+
+ <br/>
+
+ <table>
+ <tr>
+ <td>Num sockets</td>
+ <td><input type="text" id="numsockets" value="1"></td>
+ </tr>
+ <tr>
+ <td>Number of iterations</td>
+ <td><input type="text" id="numiterations" value="1"></td>
+ </tr>
+ <tr>
+ <td>Number of warm-up iterations</td>
+ <td><input type="text" id="numwarmupiterations" value="0"></td>
+ </tr>
+ <tr>
+ <td>Start size</td>
+ <td><input type="text" id="startsize" value="10240"></td>
+ </tr>
+ <tr>
+ <td>Stop threshold</td>
+ <td><input type="text" id="stopthreshold" value="102400000"></td>
+ </tr>
+ <tr>
+ <td>Minimum total</td>
+ <td><input type="text" id="mintotal" value="102400000"></td>
+ </tr>
+ <tr>
+ <td>Multipliers</td>
+ <td><input type="text" id="multipliers" value="5, 2"></td>
+ </tr>
+ </table>
+</div>
+
+<div id="log_div">
+ <textarea
+ id="log" rows="20" style="width: 100%" readonly></textarea>
+</div>
+<div id="summary_div">
+ Summary
+ <textarea
+ id="summary" rows="20" style="width: 100%" readonly></textarea>
+</div>
+
+Note: Effect of RTT is not eliminated.
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark.js b/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark.js
new file mode 100644
index 000000000..d347ae9e1
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark.js
@@ -0,0 +1,309 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the COPYING file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+if (typeof importScripts !== "undefined") {
+ // Running on a worker
+ importScripts('util.js', 'util_worker.js');
+}
+
+// Namespace for holding globals.
+var benchmark = {startTimeInMs: 0};
+
+var sockets = [];
+var numEstablishedSockets = 0;
+
+var timerID = null;
+
+function destroySocket(socket) {
+ socket.onopen = null;
+ socket.onmessage = null;
+ socket.onerror = null;
+ socket.onclose = null;
+ socket.close();
+}
+
+function destroyAllSockets() {
+ for (var i = 0; i < sockets.length; ++i) {
+ destroySocket(sockets[i]);
+ }
+ sockets = [];
+}
+
+function sendBenchmarkStep(size, config) {
+ timerID = null;
+
+ var totalSize = 0;
+ var totalReplied = 0;
+
+ var onMessageHandler = function(event) {
+ if (!verifyAcknowledgement(config, event.data, size)) {
+ destroyAllSockets();
+ return;
+ }
+
+ totalReplied += size;
+
+ if (totalReplied < totalSize) {
+ return;
+ }
+
+ calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize);
+
+ runNextTask(config);
+ };
+
+ for (var i = 0; i < sockets.length; ++i) {
+ var socket = sockets[i];
+ socket.onmessage = onMessageHandler;
+ }
+
+ var dataArray = [];
+
+ while (totalSize < config.minTotal) {
+ var buffer = new ArrayBuffer(size);
+
+ fillArrayBuffer(buffer, 0x61);
+
+ dataArray.push(buffer);
+ totalSize += size;
+ }
+
+ benchmark.startTimeInMs = getTimeStamp();
+
+ totalSize = 0;
+
+ var socketIndex = 0;
+ var dataIndex = 0;
+ while (totalSize < config.minTotal) {
+ var command = ['send'];
+ command.push(config.verifyData ? '1' : '0');
+ sockets[socketIndex].send(command.join(' '));
+ sockets[socketIndex].send(dataArray[dataIndex]);
+ socketIndex = (socketIndex + 1) % sockets.length;
+
+ totalSize += size;
+ ++dataIndex;
+ }
+}
+
+function receiveBenchmarkStep(size, config) {
+ timerID = null;
+
+ var totalSize = 0;
+ var totalReplied = 0;
+
+ var onMessageHandler = function(event) {
+ var bytesReceived = event.data.byteLength;
+ if (bytesReceived != size) {
+ config.addToLog('Expected ' + size + 'B but received ' +
+ bytesReceived + 'B');
+ destroyAllSockets();
+ return;
+ }
+
+ if (config.verifyData && !verifyArrayBuffer(event.data, 0x61)) {
+ config.addToLog('Response verification failed');
+ destroyAllSockets();
+ return;
+ }
+
+ totalReplied += bytesReceived;
+
+ if (totalReplied < totalSize) {
+ return;
+ }
+
+ calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize);
+
+ runNextTask(config);
+ };
+
+ for (var i = 0; i < sockets.length; ++i) {
+ var socket = sockets[i];
+ socket.binaryType = 'arraybuffer';
+ socket.onmessage = onMessageHandler;
+ }
+
+ benchmark.startTimeInMs = getTimeStamp();
+
+ var socketIndex = 0;
+ while (totalSize < config.minTotal) {
+ sockets[socketIndex].send('receive ' + size);
+ socketIndex = (socketIndex + 1) % sockets.length;
+
+ totalSize += size;
+ }
+}
+
+function createSocket(config) {
+ // TODO(tyoshino): Add TCP warm up.
+ var url = config.prefixUrl;
+
+ config.addToLog('Connect ' + url);
+
+ var socket = new WebSocket(url);
+ socket.onmessage = function(event) {
+ config.addToLog('Unexpected message received. Aborting.');
+ };
+ socket.onerror = function() {
+ config.addToLog('Error');
+ };
+ socket.onclose = function(event) {
+ config.addToLog('Closed');
+ };
+ return socket;
+}
+
+var tasks = [];
+
+function startBenchmark(config) {
+ clearTimeout(timerID);
+ destroyAllSockets();
+
+ numEstablishedSockets = 0;
+
+ for (var i = 0; i < config.numSockets; ++i) {
+ var socket = createSocket(config);
+ socket.onopen = function() {
+ config.addToLog('Opened');
+
+ ++numEstablishedSockets;
+
+ if (numEstablishedSockets == sockets.length) {
+ runNextTask(config);
+ }
+ };
+ sockets.push(socket);
+ }
+}
+
+function runNextTask(config) {
+ var task = tasks.shift();
+ if (task == undefined) {
+ config.addToLog('Finished');
+ destroyAllSockets();
+ return;
+ }
+ timerID = setTimeout(task, 0);
+}
+
+function buildLegendString(config) {
+ var legend = ''
+ if (config.printSize)
+ legend = 'Message size in KiB, Time/message in ms, ';
+ legend += 'Speed in kB/s';
+ return legend;
+}
+
+function getConfigString(config) {
+ return '(WebSocket' +
+ ', ' + (typeof importScripts !== "undefined" ? 'Worker' : 'Main') +
+ ', numSockets=' + config.numSockets +
+ ', numIterations=' + config.numIterations +
+ ', verifyData=' + config.verifyData +
+ ', minTotal=' + config.minTotal +
+ ', numWarmUpIterations=' + config.numWarmUpIterations +
+ ')';
+}
+
+function addTasks(config, stepFunc) {
+ for (var i = 0;
+ i < config.numWarmUpIterations + config.numIterations; ++i) {
+ // Ignore the first |config.numWarmUpIterations| iterations.
+ if (i == config.numWarmUpIterations)
+ addResultClearingTask(config);
+
+ var multiplierIndex = 0;
+ for (var size = config.startSize;
+ size <= config.stopThreshold;
+ ++multiplierIndex) {
+ var task = stepFunc.bind(
+ null,
+ size,
+ config);
+ tasks.push(task);
+ size *= config.multipliers[
+ multiplierIndex % config.multipliers.length];
+ }
+ }
+}
+
+function addResultReportingTask(config, title) {
+ tasks.push(function(){
+ timerID = null;
+ config.addToSummary(title);
+ reportAverageData(config);
+ clearAverageData();
+ runNextTask(config);
+ });
+}
+
+function addResultClearingTask(config) {
+ tasks.push(function(){
+ timerID = null;
+ clearAverageData();
+ runNextTask(config);
+ });
+}
+
+function sendBenchmark(config) {
+ config.addToLog('Send benchmark');
+ config.addToLog(buildLegendString(config));
+
+ tasks = [];
+ clearAverageData();
+ addTasks(config, sendBenchmarkStep);
+ addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config));
+ startBenchmark(config);
+}
+
+function receiveBenchmark(config) {
+ config.addToLog('Receive benchmark');
+ config.addToLog(buildLegendString(config));
+
+ tasks = [];
+ clearAverageData();
+ addTasks(config, receiveBenchmarkStep);
+ addResultReportingTask(config,
+ 'Receive Benchmark ' + getConfigString(config));
+ startBenchmark(config);
+}
+
+function batchBenchmark(config) {
+ config.addToLog('Batch benchmark');
+ config.addToLog(buildLegendString(config));
+
+ tasks = [];
+ clearAverageData();
+ addTasks(config, sendBenchmarkStep);
+ addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config));
+ addTasks(config, receiveBenchmarkStep);
+ addResultReportingTask(config, 'Receive Benchmark ' +
+ getConfigString(config));
+ startBenchmark(config);
+}
+
+function stop(config) {
+ clearTimeout(timerID);
+ timerID = null;
+ config.addToLog('Stopped');
+ destroyAllSockets();
+}
+
+onmessage = function (message) {
+ var config = message.data.config;
+ config.addToLog = workerAddToLog;
+ config.addToSummary = workerAddToSummary;
+ config.measureValue = workerMeasureValue;
+ if (message.data.type === 'sendBenchmark')
+ sendBenchmark(config);
+ else if (message.data.type === 'receiveBenchmark')
+ receiveBenchmark(config);
+ else if (message.data.type === 'batchBenchmark')
+ batchBenchmark(config);
+ else if (message.data.type === 'stop')
+ stop(config);
+};
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark_helper_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark_helper_wsh.py
new file mode 100644
index 000000000..44ad0bfee
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark_helper_wsh.py
@@ -0,0 +1,85 @@
+# Copyright 2013, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Handler for benchmark.html."""
+
+
+def web_socket_do_extra_handshake(request):
+ # Turn off compression.
+ request.ws_extension_processors = []
+
+
+def web_socket_transfer_data(request):
+ data = ''
+
+ while True:
+ command = request.ws_stream.receive_message()
+ if command is None:
+ return
+
+ if not isinstance(command, unicode):
+ raise ValueError('Invalid command data:' + command)
+ commands = command.split(' ')
+ if len(commands) == 0:
+ raise ValueError('Invalid command data: ' + command)
+
+ if commands[0] == 'receive':
+ if len(commands) != 2:
+ raise ValueError(
+ 'Illegal number of arguments for send command' +
+ command)
+ size = int(commands[1])
+
+ # Reuse data if possible.
+ if len(data) != size:
+ data = 'a' * size
+ request.ws_stream.send_message(data, binary=True)
+ elif commands[0] == 'send':
+ if len(commands) != 2:
+ raise ValueError(
+ 'Illegal number of arguments for receive command' +
+ command)
+ verify_data = commands[1] == '1'
+
+ data = request.ws_stream.receive_message()
+ if data is None:
+ raise ValueError('Payload not received')
+ size = len(data)
+
+ if verify_data:
+ if data != 'a' * size:
+ raise ValueError('Payload verification failed')
+
+ request.ws_stream.send_message(str(size))
+ else:
+ raise ValueError('Invalid command: ' + commands[0])
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/close_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/close_wsh.py
new file mode 100644
index 000000000..26b083840
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/close_wsh.py
@@ -0,0 +1,69 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import struct
+
+from mod_pywebsocket import common
+from mod_pywebsocket import stream
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ while True:
+ line = request.ws_stream.receive_message()
+ if line is None:
+ return
+ code, reason = line.split(' ', 1)
+ if code is None or reason is None:
+ return
+ request.ws_stream.close_connection(int(code), reason)
+ # close_connection() initiates closing handshake. It validates code
+ # and reason. If you want to send a broken close frame for a test,
+ # following code will be useful.
+ # > data = struct.pack('!H', int(code)) + reason.encode('UTF-8')
+ # > request.connection.write(stream.create_close_frame(data))
+ # > # Suppress to re-respond client responding close frame.
+ # > raise Exception("customized server initiated closing handshake")
+
+
+def web_socket_passive_closing_handshake(request):
+ # Simply echo a close status code
+ code, reason = request.ws_close_code, request.ws_close_reason
+
+ # pywebsocket sets pseudo code for receiving an empty body close frame.
+ if code == common.STATUS_NO_STATUS_RECEIVED:
+ code = None
+ reason = ''
+ return code, reason
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/console.html b/testing/web-platform/tests/tools/pywebsocket/src/example/console.html
new file mode 100644
index 000000000..ccd6d8f80
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/console.html
@@ -0,0 +1,317 @@
+<!--
+Copyright 2011, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+
+<!--
+A simple console for testing WebSocket server.
+
+Type an address into the top text input and click connect to establish
+WebSocket. Then, type some message into the bottom text input and click send
+to send the message. Received/sent messages and connection state will be shown
+on the middle textarea.
+-->
+
+<html>
+<head>
+<title>WebSocket console</title>
+<script>
+var socket = null;
+
+var showTimeStamp = false;
+
+var addressBox = null;
+var protocolsBox = null;
+var logBox = null;
+var messageBox = null;
+var fileBox = null;
+var codeBox = null;
+var reasonBox = null;
+
+function getTimeStamp() {
+ return new Date().getTime();
+}
+
+function addToLog(log) {
+ if (showTimeStamp) {
+ logBox.value += '[' + getTimeStamp() + '] ';
+ }
+ logBox.value += log + '\n'
+ // Large enough to keep showing the latest message.
+ logBox.scrollTop = 1000000;
+}
+
+function setbinarytype(binaryType) {
+ if (!socket) {
+ addToLog('Not connected');
+ return;
+ }
+
+ socket.binaryType = binaryType;
+ addToLog('Set binaryType to ' + binaryType);
+}
+
+function send() {
+ if (!socket) {
+ addToLog('Not connected');
+ return;
+ }
+
+ socket.send(messageBox.value);
+ addToLog('> ' + messageBox.value);
+ messageBox.value = '';
+}
+
+function sendfile() {
+ if (!socket) {
+ addToLog('Not connected');
+ return;
+ }
+
+ var files = fileBox.files;
+
+ if (files.length == 0) {
+ addToLog('File not selected');
+ return;
+ }
+
+ socket.send(files[0]);
+ addToLog('> Send ' + files[0].name);
+}
+
+function parseProtocols(protocolsText) {
+ var protocols = protocolsText.split(',');
+ for (var i = 0; i < protocols.length; ++i) {
+ protocols[i] = protocols[i].trim();
+ }
+
+ if (protocols.length == 0) {
+ // Don't pass.
+ protocols = null;
+ } else if (protocols.length == 1) {
+ if (protocols[0].length == 0) {
+ // Don't pass.
+ protocols = null;
+ } else {
+ // Pass as a string.
+ protocols = protocols[0];
+ }
+ }
+
+ return protocols;
+}
+
+function connect() {
+ var url = addressBox.value;
+ var protocols = parseProtocols(protocolsBox.value);
+
+ if ('WebSocket' in window) {
+ if (protocols) {
+ socket = new WebSocket(url, protocols);
+ } else {
+ socket = new WebSocket(url);
+ }
+ } else {
+ return;
+ }
+
+ socket.onopen = function () {
+ var extraInfo = [];
+ if (('protocol' in socket) && socket.protocol) {
+ extraInfo.push('protocol = ' + socket.protocol);
+ }
+ if (('extensions' in socket) && socket.extensions) {
+ extraInfo.push('extensions = ' + socket.extensions);
+ }
+
+ var logMessage = 'Opened';
+ if (extraInfo.length > 0) {
+ logMessage += ' (' + extraInfo.join(', ') + ')';
+ }
+ addToLog(logMessage);
+ };
+ socket.onmessage = function (event) {
+ if (('ArrayBuffer' in window) && (event.data instanceof ArrayBuffer)) {
+ addToLog('< Received an ArrayBuffer of ' + event.data.byteLength +
+ ' bytes')
+ } else if (('Blob' in window) && (event.data instanceof Blob)) {
+ addToLog('< Received a Blob of ' + event.data.size + ' bytes')
+ } else {
+ addToLog('< ' + event.data);
+ }
+ };
+ socket.onerror = function () {
+ addToLog('Error');
+ };
+ socket.onclose = function (event) {
+ var logMessage = 'Closed (';
+ if ((arguments.length == 1) && ('CloseEvent' in window) &&
+ (event instanceof CloseEvent)) {
+ logMessage += 'wasClean = ' + event.wasClean;
+ // code and reason are present only for
+ // draft-ietf-hybi-thewebsocketprotocol-06 and later
+ if ('code' in event) {
+ logMessage += ', code = ' + event.code;
+ }
+ if ('reason' in event) {
+ logMessage += ', reason = ' + event.reason;
+ }
+ } else {
+ logMessage += 'CloseEvent is not available';
+ }
+ addToLog(logMessage + ')');
+ };
+
+ if (protocols) {
+ addToLog('Connect ' + url + ' (protocols = ' + protocols + ')');
+ } else {
+ addToLog('Connect ' + url);
+ }
+}
+
+function closeSocket() {
+ if (!socket) {
+ addToLog('Not connected');
+ return;
+ }
+
+ if (codeBox.value || reasonBox.value) {
+ socket.close(codeBox.value, reasonBox.value);
+ } else {
+ socket.close();
+ }
+}
+
+function printState() {
+ if (!socket) {
+ addToLog('Not connected');
+ return;
+ }
+
+ addToLog(
+ 'url = ' + socket.url +
+ ', readyState = ' + socket.readyState +
+ ', bufferedAmount = ' + socket.bufferedAmount);
+}
+
+function init() {
+ var scheme = window.location.protocol == 'https:' ? 'wss://' : 'ws://';
+ var defaultAddress = scheme + window.location.host + '/echo';
+
+ addressBox = document.getElementById('address');
+ protocolsBox = document.getElementById('protocols');
+ logBox = document.getElementById('log');
+ messageBox = document.getElementById('message');
+ fileBox = document.getElementById('file');
+ codeBox = document.getElementById('code');
+ reasonBox = document.getElementById('reason');
+
+ addressBox.value = defaultAddress;
+
+ if (!('WebSocket' in window)) {
+ addToLog('WebSocket is not available');
+ }
+}
+</script>
+<style type="text/css">
+form {
+ margin: 0px;
+}
+
+#connect_div, #log_div, #send_div, #sendfile_div, #close_div, #printstate_div {
+ padding: 5px;
+ margin: 5px;
+ border-width: 0px 0px 0px 10px;
+ border-style: solid;
+ border-color: silver;
+}
+</style>
+</head>
+<body onload="init()">
+
+<div>
+
+<div id="connect_div">
+ <form action="#" onsubmit="connect(); return false;">
+ url <input type="text" id="address" size="40">
+ <input type="submit" value="connect">
+ <br/>
+ protocols <input type="text" id="protocols" size="20">
+ </form>
+</div>
+
+<div id="log_div">
+ <textarea id="log" rows="10" cols="40" readonly></textarea>
+ <br/>
+ <input type="checkbox"
+ name="showtimestamp"
+ value="showtimestamp"
+ onclick="showTimeStamp = this.checked">Show time stamp
+</div>
+
+<div id="send_div">
+ <form action="#" onsubmit="send(); return false;">
+ data <input type="text" id="message" size="40">
+ <input type="submit" value="send">
+ </form>
+</div>
+
+<div id="sendfile_div">
+ <form action="#" onsubmit="sendfile(); return false;">
+ <input type="file" id="file" size="40">
+ <input type="submit" value="send file">
+ </form>
+
+ Set binaryType
+ <input type="radio"
+ name="binarytype"
+ value="blob"
+ onclick="setbinarytype('blob')" checked>blob
+ <input type="radio"
+ name="binarytype"
+ value="arraybuffer"
+ onclick="setbinarytype('arraybuffer')">arraybuffer
+</div>
+
+<div id="close_div">
+ <form action="#" onsubmit="closeSocket(); return false;">
+ code <input type="text" id="code" size="10">
+ reason <input type="text" id="reason" size="20">
+ <input type="submit" value="close">
+ </form>
+</div>
+
+<div id="printstate_div">
+ <input type="button" value="print state" onclick="printState();">
+</div>
+
+</div>
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/cookie_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/cookie_wsh.py
new file mode 100644
index 000000000..8b327152e
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/cookie_wsh.py
@@ -0,0 +1,32 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the COPYING file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+
+import urlparse
+
+
+def _add_set_cookie(request, value):
+ request.extra_headers.append(('Set-Cookie', value))
+
+
+def web_socket_do_extra_handshake(request):
+ components = urlparse.urlparse(request.uri)
+ command = components[4]
+
+ ONE_DAY_LIFE = 'Max-Age=86400'
+
+ if command == 'set':
+ _add_set_cookie(request, '; '.join(['foo=bar', ONE_DAY_LIFE]))
+ elif command == 'set_httponly':
+ _add_set_cookie(request,
+ '; '.join(['httpOnlyFoo=bar', ONE_DAY_LIFE, 'httpOnly']))
+ elif command == 'clear':
+ _add_set_cookie(request, 'foo=0; Max-Age=0')
+ _add_set_cookie(request, 'httpOnlyFoo=0; Max-Age=0')
+
+
+def web_socket_transfer_data(request):
+ pass
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/echo_client.py b/testing/web-platform/tests/tools/pywebsocket/src/example/echo_client.py
new file mode 100755
index 000000000..943ce64e8
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/echo_client.py
@@ -0,0 +1,1128 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Simple WebSocket client named echo_client just because of historical reason.
+
+mod_pywebsocket directory must be in PYTHONPATH.
+
+Example Usage:
+
+# server setup
+ % cd $pywebsocket
+ % PYTHONPATH=$cwd/src python ./mod_pywebsocket/standalone.py -p 8880 \
+ -d $cwd/src/example
+
+# run client
+ % PYTHONPATH=$cwd/src python ./src/example/echo_client.py -p 8880 \
+ -s localhost \
+ -o http://localhost -r /echo -m test
+
+or
+
+# run echo client to test IETF HyBi 00 protocol
+ run with --protocol-version=hybi00
+"""
+
+
+import base64
+import codecs
+import logging
+from optparse import OptionParser
+import os
+import random
+import re
+import socket
+import struct
+import sys
+
+from mod_pywebsocket import common
+from mod_pywebsocket.extensions import DeflateFrameExtensionProcessor
+from mod_pywebsocket.extensions import PerMessageDeflateExtensionProcessor
+from mod_pywebsocket.extensions import _PerMessageDeflateFramer
+from mod_pywebsocket.extensions import _parse_window_bits
+from mod_pywebsocket.stream import Stream
+from mod_pywebsocket.stream import StreamHixie75
+from mod_pywebsocket.stream import StreamOptions
+from mod_pywebsocket import util
+
+
+_TIMEOUT_SEC = 10
+_UNDEFINED_PORT = -1
+
+_UPGRADE_HEADER = 'Upgrade: websocket\r\n'
+_UPGRADE_HEADER_HIXIE75 = 'Upgrade: WebSocket\r\n'
+_CONNECTION_HEADER = 'Connection: Upgrade\r\n'
+
+# Special message that tells the echo server to start closing handshake
+_GOODBYE_MESSAGE = 'Goodbye'
+
+_PROTOCOL_VERSION_HYBI13 = 'hybi13'
+_PROTOCOL_VERSION_HYBI08 = 'hybi08'
+_PROTOCOL_VERSION_HYBI00 = 'hybi00'
+_PROTOCOL_VERSION_HIXIE75 = 'hixie75'
+
+# Constants for the --tls_module flag.
+_TLS_BY_STANDARD_MODULE = 'ssl'
+_TLS_BY_PYOPENSSL = 'pyopenssl'
+
+# Values used by the --tls-version flag.
+_TLS_VERSION_SSL23 = 'ssl23'
+_TLS_VERSION_SSL3 = 'ssl3'
+_TLS_VERSION_TLS1 = 'tls1'
+
+
+class ClientHandshakeError(Exception):
+ pass
+
+
+def _build_method_line(resource):
+ return 'GET %s HTTP/1.1\r\n' % resource
+
+
+def _origin_header(header, origin):
+ # 4.1 13. concatenation of the string "Origin:", a U+0020 SPACE character,
+ # and the /origin/ value, converted to ASCII lowercase, to /fields/.
+ return '%s: %s\r\n' % (header, origin.lower())
+
+
+def _format_host_header(host, port, secure):
+ # 4.1 9. Let /hostport/ be an empty string.
+ # 4.1 10. Append the /host/ value, converted to ASCII lowercase, to
+ # /hostport/
+ hostport = host.lower()
+ # 4.1 11. If /secure/ is false, and /port/ is not 80, or if /secure/
+ # is true, and /port/ is not 443, then append a U+003A COLON character
+ # (:) followed by the value of /port/, expressed as a base-ten integer,
+ # to /hostport/
+ if ((not secure and port != common.DEFAULT_WEB_SOCKET_PORT) or
+ (secure and port != common.DEFAULT_WEB_SOCKET_SECURE_PORT)):
+ hostport += ':' + str(port)
+ # 4.1 12. concatenation of the string "Host:", a U+0020 SPACE
+ # character, and /hostport/, to /fields/.
+ return '%s: %s\r\n' % (common.HOST_HEADER, hostport)
+
+
+def _receive_bytes(socket, length):
+ bytes = []
+ remaining = length
+ while remaining > 0:
+ received_bytes = socket.recv(remaining)
+ if not received_bytes:
+ raise IOError(
+ 'Connection closed before receiving requested length '
+ '(requested %d bytes but received only %d bytes)' %
+ (length, length - remaining))
+ bytes.append(received_bytes)
+ remaining -= len(received_bytes)
+ return ''.join(bytes)
+
+
+def _get_mandatory_header(fields, name):
+ """Gets the value of the header specified by name from fields.
+
+ This function expects that there's only one header with the specified name
+ in fields. Otherwise, raises an ClientHandshakeError.
+ """
+
+ values = fields.get(name.lower())
+ if values is None or len(values) == 0:
+ raise ClientHandshakeError(
+ '%s header not found: %r' % (name, values))
+ if len(values) > 1:
+ raise ClientHandshakeError(
+ 'Multiple %s headers found: %r' % (name, values))
+ return values[0]
+
+
+def _validate_mandatory_header(fields, name,
+ expected_value, case_sensitive=False):
+ """Gets and validates the value of the header specified by name from
+ fields.
+
+ If expected_value is specified, compares expected value and actual value
+ and raises an ClientHandshakeError on failure. You can specify case
+ sensitiveness in this comparison by case_sensitive parameter. This function
+ expects that there's only one header with the specified name in fields.
+ Otherwise, raises an ClientHandshakeError.
+ """
+
+ value = _get_mandatory_header(fields, name)
+
+ if ((case_sensitive and value != expected_value) or
+ (not case_sensitive and value.lower() != expected_value.lower())):
+ raise ClientHandshakeError(
+ 'Illegal value for header %s: %r (expected) vs %r (actual)' %
+ (name, expected_value, value))
+
+
+class _TLSSocket(object):
+ """Wrapper for a TLS connection."""
+
+ def __init__(self,
+ raw_socket, tls_module, tls_version, disable_tls_compression):
+ self._logger = util.get_class_logger(self)
+
+ if tls_module == _TLS_BY_STANDARD_MODULE:
+ if tls_version == _TLS_VERSION_SSL23:
+ version = ssl.PROTOCOL_SSLv23
+ elif tls_version == _TLS_VERSION_SSL3:
+ version = ssl.PROTOCOL_SSLv3
+ elif tls_version == _TLS_VERSION_TLS1:
+ version = ssl.PROTOCOL_TLSv1
+ else:
+ raise ValueError(
+ 'Invalid --tls-version flag: %r' % tls_version)
+
+ if disable_tls_compression:
+ raise ValueError(
+ '--disable-tls-compression is not available for ssl '
+ 'module')
+
+ self._tls_socket = ssl.wrap_socket(raw_socket, ssl_version=version)
+
+ # Print cipher in use. Handshake is done on wrap_socket call.
+ self._logger.info("Cipher: %s", self._tls_socket.cipher())
+ elif tls_module == _TLS_BY_PYOPENSSL:
+ if tls_version == _TLS_VERSION_SSL23:
+ version = OpenSSL.SSL.SSLv23_METHOD
+ elif tls_version == _TLS_VERSION_SSL3:
+ version = OpenSSL.SSL.SSLv3_METHOD
+ elif tls_version == _TLS_VERSION_TLS1:
+ version = OpenSSL.SSL.TLSv1_METHOD
+ else:
+ raise ValueError(
+ 'Invalid --tls-version flag: %r' % tls_version)
+
+ context = OpenSSL.SSL.Context(version)
+
+ if disable_tls_compression:
+ # OP_NO_COMPRESSION is not defined in OpenSSL module.
+ context.set_options(0x00020000)
+
+ self._tls_socket = OpenSSL.SSL.Connection(context, raw_socket)
+ # Client mode.
+ self._tls_socket.set_connect_state()
+ self._tls_socket.setblocking(True)
+
+ # Do handshake now (not necessary).
+ self._tls_socket.do_handshake()
+ else:
+ raise ValueError('No TLS support module is available')
+
+ def send(self, data):
+ return self._tls_socket.write(data)
+
+ def sendall(self, data):
+ return self._tls_socket.sendall(data)
+
+ def recv(self, size=-1):
+ return self._tls_socket.read(size)
+
+ def close(self):
+ return self._tls_socket.close()
+
+ def getpeername(self):
+ return self._tls_socket.getpeername()
+
+
+class ClientHandshakeBase(object):
+ """A base class for WebSocket opening handshake processors for each
+ protocol version.
+ """
+
+ def __init__(self):
+ self._logger = util.get_class_logger(self)
+
+ def _read_fields(self):
+ # 4.1 32. let /fields/ be a list of name-value pairs, initially empty.
+ fields = {}
+ while True: # "Field"
+ # 4.1 33. let /name/ and /value/ be empty byte arrays
+ name = ''
+ value = ''
+ # 4.1 34. read /name/
+ name = self._read_name()
+ if name is None:
+ break
+ # 4.1 35. read spaces
+ # TODO(tyoshino): Skip only one space as described in the spec.
+ ch = self._skip_spaces()
+ # 4.1 36. read /value/
+ value = self._read_value(ch)
+ # 4.1 37. read a byte from the server
+ ch = _receive_bytes(self._socket, 1)
+ if ch != '\n': # 0x0A
+ raise ClientHandshakeError(
+ 'Expected LF but found %r while reading value %r for '
+ 'header %r' % (ch, value, name))
+ self._logger.debug('Received %r header', name)
+ # 4.1 38. append an entry to the /fields/ list that has the name
+ # given by the string obtained by interpreting the /name/ byte
+ # array as a UTF-8 stream and the value given by the string
+ # obtained by interpreting the /value/ byte array as a UTF-8 byte
+ # stream.
+ fields.setdefault(name, []).append(value)
+ # 4.1 39. return to the "Field" step above
+ return fields
+
+ def _read_name(self):
+ # 4.1 33. let /name/ be empty byte arrays
+ name = ''
+ while True:
+ # 4.1 34. read a byte from the server
+ ch = _receive_bytes(self._socket, 1)
+ if ch == '\r': # 0x0D
+ return None
+ elif ch == '\n': # 0x0A
+ raise ClientHandshakeError(
+ 'Unexpected LF when reading header name %r' % name)
+ elif ch == ':': # 0x3A
+ return name
+ elif ch >= 'A' and ch <= 'Z': # Range 0x31 to 0x5A
+ ch = chr(ord(ch) + 0x20)
+ name += ch
+ else:
+ name += ch
+
+ def _skip_spaces(self):
+ # 4.1 35. read a byte from the server
+ while True:
+ ch = _receive_bytes(self._socket, 1)
+ if ch == ' ': # 0x20
+ continue
+ return ch
+
+ def _read_value(self, ch):
+ # 4.1 33. let /value/ be empty byte arrays
+ value = ''
+ # 4.1 36. read a byte from server.
+ while True:
+ if ch == '\r': # 0x0D
+ return value
+ elif ch == '\n': # 0x0A
+ raise ClientHandshakeError(
+ 'Unexpected LF when reading header value %r' % value)
+ else:
+ value += ch
+ ch = _receive_bytes(self._socket, 1)
+
+
+def _get_permessage_deflate_framer(extension_response):
+ """Validate the response and return a framer object using the parameters in
+ the response. This method doesn't accept the server_.* parameters.
+ """
+
+ client_max_window_bits = None
+ client_no_context_takeover = None
+
+ client_max_window_bits_name = (
+ PerMessageDeflateExtensionProcessor.
+ _CLIENT_MAX_WINDOW_BITS_PARAM)
+ client_no_context_takeover_name = (
+ PerMessageDeflateExtensionProcessor.
+ _CLIENT_NO_CONTEXT_TAKEOVER_PARAM)
+
+ # We didn't send any server_.* parameter.
+ # Handle those parameters as invalid if found in the response.
+
+ for param_name, param_value in extension_response.get_parameters():
+ if param_name == client_max_window_bits_name:
+ if client_max_window_bits is not None:
+ raise ClientHandshakeError(
+ 'Multiple %s found' % client_max_window_bits_name)
+
+ parsed_value = _parse_window_bits(param_value)
+ if parsed_value is None:
+ raise ClientHandshakeError(
+ 'Bad %s: %r' %
+ (client_max_window_bits_name, param_value))
+ client_max_window_bits = parsed_value
+ elif param_name == client_no_context_takeover_name:
+ if client_no_context_takeover is not None:
+ raise ClientHandshakeError(
+ 'Multiple %s found' % client_no_context_takeover_name)
+
+ if param_value is not None:
+ raise ClientHandshakeError(
+ 'Bad %s: Has value %r' %
+ (client_no_context_takeover_name, param_value))
+ client_no_context_takeover = True
+
+ if client_no_context_takeover is None:
+ client_no_context_takeover = False
+
+ return _PerMessageDeflateFramer(client_max_window_bits,
+ client_no_context_takeover)
+
+
+class ClientHandshakeProcessor(ClientHandshakeBase):
+ """WebSocket opening handshake processor for
+ draft-ietf-hybi-thewebsocketprotocol-06 and later.
+ """
+
+ def __init__(self, socket, options):
+ super(ClientHandshakeProcessor, self).__init__()
+
+ self._socket = socket
+ self._options = options
+
+ self._logger = util.get_class_logger(self)
+
+ def handshake(self):
+ """Performs opening handshake on the specified socket.
+
+ Raises:
+ ClientHandshakeError: handshake failed.
+ """
+
+ request_line = _build_method_line(self._options.resource)
+ self._logger.debug('Client\'s opening handshake Request-Line: %r',
+ request_line)
+ self._socket.sendall(request_line)
+
+ fields = []
+ fields.append(_format_host_header(
+ self._options.server_host,
+ self._options.server_port,
+ self._options.use_tls))
+ fields.append(_UPGRADE_HEADER)
+ fields.append(_CONNECTION_HEADER)
+ if self._options.origin is not None:
+ if self._options.protocol_version == _PROTOCOL_VERSION_HYBI08:
+ fields.append(_origin_header(
+ common.SEC_WEBSOCKET_ORIGIN_HEADER,
+ self._options.origin))
+ else:
+ fields.append(_origin_header(common.ORIGIN_HEADER,
+ self._options.origin))
+
+ original_key = os.urandom(16)
+ self._key = base64.b64encode(original_key)
+ self._logger.debug(
+ '%s: %r (%s)',
+ common.SEC_WEBSOCKET_KEY_HEADER,
+ self._key,
+ util.hexify(original_key))
+ fields.append(
+ '%s: %s\r\n' % (common.SEC_WEBSOCKET_KEY_HEADER, self._key))
+
+ if self._options.version_header > 0:
+ fields.append('%s: %d\r\n' % (common.SEC_WEBSOCKET_VERSION_HEADER,
+ self._options.version_header))
+ elif self._options.protocol_version == _PROTOCOL_VERSION_HYBI08:
+ fields.append('%s: %d\r\n' % (common.SEC_WEBSOCKET_VERSION_HEADER,
+ common.VERSION_HYBI08))
+ else:
+ fields.append('%s: %d\r\n' % (common.SEC_WEBSOCKET_VERSION_HEADER,
+ common.VERSION_HYBI_LATEST))
+
+ extensions_to_request = []
+
+ if self._options.deflate_frame:
+ extensions_to_request.append(
+ common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION))
+
+ if self._options.use_permessage_deflate:
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ # Accept the client_max_window_bits extension parameter by default.
+ extension.add_parameter(
+ PerMessageDeflateExtensionProcessor.
+ _CLIENT_MAX_WINDOW_BITS_PARAM,
+ None)
+ extensions_to_request.append(extension)
+
+ if len(extensions_to_request) != 0:
+ fields.append(
+ '%s: %s\r\n' %
+ (common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
+ common.format_extensions(extensions_to_request)))
+
+ for field in fields:
+ self._socket.sendall(field)
+
+ self._socket.sendall('\r\n')
+
+ self._logger.debug('Sent client\'s opening handshake headers: %r',
+ fields)
+ self._logger.debug('Start reading Status-Line')
+
+ status_line = ''
+ while True:
+ ch = _receive_bytes(self._socket, 1)
+ status_line += ch
+ if ch == '\n':
+ break
+
+ m = re.match('HTTP/\\d+\.\\d+ (\\d\\d\\d) .*\r\n', status_line)
+ if m is None:
+ raise ClientHandshakeError(
+ 'Wrong status line format: %r' % status_line)
+ status_code = m.group(1)
+ if status_code != '101':
+ self._logger.debug('Unexpected status code %s with following '
+ 'headers: %r', status_code, self._read_fields())
+ raise ClientHandshakeError(
+ 'Expected HTTP status code 101 but found %r' % status_code)
+
+ self._logger.debug('Received valid Status-Line')
+ self._logger.debug('Start reading headers until we see an empty line')
+
+ fields = self._read_fields()
+
+ ch = _receive_bytes(self._socket, 1)
+ if ch != '\n': # 0x0A
+ raise ClientHandshakeError(
+ 'Expected LF but found %r while reading value %r for header '
+ 'name %r' % (ch, value, name))
+
+ self._logger.debug('Received an empty line')
+ self._logger.debug('Server\'s opening handshake headers: %r', fields)
+
+ _validate_mandatory_header(
+ fields,
+ common.UPGRADE_HEADER,
+ common.WEBSOCKET_UPGRADE_TYPE,
+ False)
+
+ _validate_mandatory_header(
+ fields,
+ common.CONNECTION_HEADER,
+ common.UPGRADE_CONNECTION_TYPE,
+ False)
+
+ accept = _get_mandatory_header(
+ fields, common.SEC_WEBSOCKET_ACCEPT_HEADER)
+
+ # Validate
+ try:
+ binary_accept = base64.b64decode(accept)
+ except TypeError, e:
+ raise HandshakeError(
+ 'Illegal value for header %s: %r' %
+ (common.SEC_WEBSOCKET_ACCEPT_HEADER, accept))
+
+ if len(binary_accept) != 20:
+ raise ClientHandshakeError(
+ 'Decoded value of %s is not 20-byte long' %
+ common.SEC_WEBSOCKET_ACCEPT_HEADER)
+
+ self._logger.debug(
+ 'Response for challenge : %r (%s)',
+ accept, util.hexify(binary_accept))
+
+ binary_expected_accept = util.sha1_hash(
+ self._key + common.WEBSOCKET_ACCEPT_UUID).digest()
+ expected_accept = base64.b64encode(binary_expected_accept)
+
+ self._logger.debug(
+ 'Expected response for challenge: %r (%s)',
+ expected_accept, util.hexify(binary_expected_accept))
+
+ if accept != expected_accept:
+ raise ClientHandshakeError(
+ 'Invalid %s header: %r (expected: %s)' %
+ (common.SEC_WEBSOCKET_ACCEPT_HEADER, accept, expected_accept))
+
+ deflate_frame_accepted = False
+ permessage_deflate_accepted = False
+
+ extensions_header = fields.get(
+ common.SEC_WEBSOCKET_EXTENSIONS_HEADER.lower())
+ accepted_extensions = []
+ if extensions_header is not None and len(extensions_header) != 0:
+ accepted_extensions = common.parse_extensions(extensions_header[0])
+
+ # TODO(bashi): Support the new style perframe compression extension.
+ for extension in accepted_extensions:
+ extension_name = extension.name()
+ if (extension_name == common.DEFLATE_FRAME_EXTENSION and
+ self._options.deflate_frame):
+ deflate_frame_accepted = True
+ processor = DeflateFrameExtensionProcessor(extension)
+ unused_extension_response = processor.get_extension_response()
+ self._options.deflate_frame = processor
+ continue
+ elif (extension_name == common.PERMESSAGE_DEFLATE_EXTENSION and
+ self._options.use_permessage_deflate):
+ permessage_deflate_accepted = True
+
+ framer = _get_permessage_deflate_framer(extension)
+ framer.set_compress_outgoing_enabled(True)
+ self._options.use_permessage_deflate = framer
+ continue
+
+ raise ClientHandshakeError(
+ 'Unexpected extension %r' % extension_name)
+
+ if (self._options.deflate_frame and not deflate_frame_accepted):
+ raise ClientHandshakeError(
+ 'Requested %s, but the server rejected it' %
+ common.DEFLATE_FRAME_EXTENSION)
+
+ if (self._options.use_permessage_deflate and
+ not permessage_deflate_accepted):
+ raise ClientHandshakeError(
+ 'Requested %s, but the server rejected it' %
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+
+ # TODO(tyoshino): Handle Sec-WebSocket-Protocol
+ # TODO(tyoshino): Handle Cookie, etc.
+
+
+class ClientHandshakeProcessorHybi00(ClientHandshakeBase):
+ """WebSocket opening handshake processor for
+ draft-ietf-hybi-thewebsocketprotocol-00 (equivalent to
+ draft-hixie-thewebsocketprotocol-76).
+ """
+
+ def __init__(self, socket, options):
+ super(ClientHandshakeProcessorHybi00, self).__init__()
+
+ self._socket = socket
+ self._options = options
+
+ self._logger = util.get_class_logger(self)
+
+ if (self._options.deflate_frame or
+ self._options.use_permessage_deflate):
+ logging.critical('HyBi 00 doesn\'t support extensions.')
+ sys.exit(1)
+
+ def handshake(self):
+ """Performs opening handshake on the specified socket.
+
+ Raises:
+ ClientHandshakeError: handshake failed.
+ """
+
+ # 4.1 5. send request line.
+ self._socket.sendall(_build_method_line(self._options.resource))
+ # 4.1 6. Let /fields/ be an empty list of strings.
+ fields = []
+ # 4.1 7. Add the string "Upgrade: WebSocket" to /fields/.
+ fields.append(_UPGRADE_HEADER_HIXIE75)
+ # 4.1 8. Add the string "Connection: Upgrade" to /fields/.
+ fields.append(_CONNECTION_HEADER)
+ # 4.1 9-12. Add Host: field to /fields/.
+ fields.append(_format_host_header(
+ self._options.server_host,
+ self._options.server_port,
+ self._options.use_tls))
+ # 4.1 13. Add Origin: field to /fields/.
+ if not self._options.origin:
+ raise ClientHandshakeError(
+ 'Specify the origin of the connection by --origin flag')
+ fields.append(_origin_header(common.ORIGIN_HEADER,
+ self._options.origin))
+ # TODO: 4.1 14 Add Sec-WebSocket-Protocol: field to /fields/.
+ # TODO: 4.1 15 Add cookie headers to /fields/.
+
+ # 4.1 16-23. Add Sec-WebSocket-Key<n> to /fields/.
+ self._number1, key1 = self._generate_sec_websocket_key()
+ self._logger.debug('Number1: %d', self._number1)
+ fields.append('%s: %s\r\n' % (common.SEC_WEBSOCKET_KEY1_HEADER, key1))
+ self._number2, key2 = self._generate_sec_websocket_key()
+ self._logger.debug('Number2: %d', self._number2)
+ fields.append('%s: %s\r\n' % (common.SEC_WEBSOCKET_KEY2_HEADER, key2))
+
+ fields.append('%s: 0\r\n' % common.SEC_WEBSOCKET_DRAFT_HEADER)
+
+ # 4.1 24. For each string in /fields/, in a random order: send the
+ # string, encoded as UTF-8, followed by a UTF-8 encoded U+000D CARRIAGE
+ # RETURN U+000A LINE FEED character pair (CRLF).
+ random.shuffle(fields)
+ for field in fields:
+ self._socket.sendall(field)
+ # 4.1 25. send a UTF-8-encoded U+000D CARRIAGE RETURN U+000A LINE FEED
+ # character pair (CRLF).
+ self._socket.sendall('\r\n')
+ # 4.1 26. let /key3/ be a string consisting of eight random bytes (or
+ # equivalently, a random 64 bit integer encoded in a big-endian order).
+ self._key3 = self._generate_key3()
+ # 4.1 27. send /key3/ to the server.
+ self._socket.sendall(self._key3)
+ self._logger.debug(
+ 'Key3: %r (%s)', self._key3, util.hexify(self._key3))
+
+ self._logger.info('Sent handshake')
+
+ # 4.1 28. Read bytes from the server until either the connection
+ # closes, or a 0x0A byte is read. let /field/ be these bytes, including
+ # the 0x0A bytes.
+ field = ''
+ while True:
+ ch = _receive_bytes(self._socket, 1)
+ field += ch
+ if ch == '\n':
+ break
+ # if /field/ is not at least seven bytes long, or if the last
+ # two bytes aren't 0x0D and 0x0A respectively, or if it does not
+ # contain at least two 0x20 bytes, then fail the WebSocket connection
+ # and abort these steps.
+ if len(field) < 7 or not field.endswith('\r\n'):
+ raise ClientHandshakeError('Wrong status line: %r' % field)
+ m = re.match('[^ ]* ([^ ]*) .*', field)
+ if m is None:
+ raise ClientHandshakeError(
+ 'No HTTP status code found in status line: %r' % field)
+ # 4.1 29. let /code/ be the substring of /field/ that starts from the
+ # byte after the first 0x20 byte, and ends with the byte before the
+ # second 0x20 byte.
+ code = m.group(1)
+ # 4.1 30. if /code/ is not three bytes long, or if any of the bytes in
+ # /code/ are not in the range 0x30 to 0x90, then fail the WebSocket
+ # connection and abort these steps.
+ if not re.match('[0-9][0-9][0-9]', code):
+ raise ClientHandshakeError(
+ 'HTTP status code %r is not three digit in status line: %r' %
+ (code, field))
+ # 4.1 31. if /code/, interpreted as UTF-8, is "101", then move to the
+ # next step.
+ if code != '101':
+ raise ClientHandshakeError(
+ 'Expected HTTP status code 101 but found %r in status line: '
+ '%r' % (code, field))
+ # 4.1 32-39. read fields into /fields/
+ fields = self._read_fields()
+ # 4.1 40. _Fields processing_
+ # read a byte from server
+ ch = _receive_bytes(self._socket, 1)
+ if ch != '\n': # 0x0A
+ raise ClientHandshakeError('Expected LF but found %r' % ch)
+ # 4.1 41. check /fields/
+ # TODO(ukai): protocol
+ # if the entry's name is "upgrade"
+ # if the value is not exactly equal to the string "WebSocket",
+ # then fail the WebSocket connection and abort these steps.
+ _validate_mandatory_header(
+ fields,
+ common.UPGRADE_HEADER,
+ common.WEBSOCKET_UPGRADE_TYPE_HIXIE75,
+ True)
+ # if the entry's name is "connection"
+ # if the value, converted to ASCII lowercase, is not exactly equal
+ # to the string "upgrade", then fail the WebSocket connection and
+ # abort these steps.
+ _validate_mandatory_header(
+ fields,
+ common.CONNECTION_HEADER,
+ common.UPGRADE_CONNECTION_TYPE,
+ False)
+
+ origin = _get_mandatory_header(
+ fields, common.SEC_WEBSOCKET_ORIGIN_HEADER)
+
+ location = _get_mandatory_header(
+ fields, common.SEC_WEBSOCKET_LOCATION_HEADER)
+
+ # TODO(ukai): check origin, location, cookie, ..
+
+ # 4.1 42. let /challenge/ be the concatenation of /number_1/,
+ # expressed as a big endian 32 bit integer, /number_2/, expressed
+ # as big endian 32 bit integer, and the eight bytes of /key_3/ in the
+ # order they were sent on the wire.
+ challenge = struct.pack('!I', self._number1)
+ challenge += struct.pack('!I', self._number2)
+ challenge += self._key3
+
+ self._logger.debug(
+ 'Challenge: %r (%s)', challenge, util.hexify(challenge))
+
+ # 4.1 43. let /expected/ be the MD5 fingerprint of /challenge/ as a
+ # big-endian 128 bit string.
+ expected = util.md5_hash(challenge).digest()
+ self._logger.debug(
+ 'Expected challenge response: %r (%s)',
+ expected, util.hexify(expected))
+
+ # 4.1 44. read sixteen bytes from the server.
+ # let /reply/ be those bytes.
+ reply = _receive_bytes(self._socket, 16)
+ self._logger.debug(
+ 'Actual challenge response: %r (%s)', reply, util.hexify(reply))
+
+ # 4.1 45. if /reply/ does not exactly equal /expected/, then fail
+ # the WebSocket connection and abort these steps.
+ if expected != reply:
+ raise ClientHandshakeError(
+ 'Bad challenge response: %r (expected) != %r (actual)' %
+ (expected, reply))
+ # 4.1 46. The *WebSocket connection is established*.
+
+ def _generate_sec_websocket_key(self):
+ # 4.1 16. let /spaces_n/ be a random integer from 1 to 12 inclusive.
+ spaces = random.randint(1, 12)
+ # 4.1 17. let /max_n/ be the largest integer not greater than
+ # 4,294,967,295 divided by /spaces_n/.
+ maxnum = 4294967295 / spaces
+ # 4.1 18. let /number_n/ be a random integer from 0 to /max_n/
+ # inclusive.
+ number = random.randint(0, maxnum)
+ # 4.1 19. let /product_n/ be the result of multiplying /number_n/ and
+ # /spaces_n/ together.
+ product = number * spaces
+ # 4.1 20. let /key_n/ be a string consisting of /product_n/, expressed
+ # in base ten using the numerals in the range U+0030 DIGIT ZERO (0) to
+ # U+0039 DIGIT NINE (9).
+ key = str(product)
+ # 4.1 21. insert between one and twelve random characters from the
+ # range U+0021 to U+002F and U+003A to U+007E into /key_n/ at random
+ # positions.
+ available_chars = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1)
+ n = random.randint(1, 12)
+ for _ in xrange(n):
+ ch = random.choice(available_chars)
+ pos = random.randint(0, len(key))
+ key = key[0:pos] + chr(ch) + key[pos:]
+ # 4.1 22. insert /spaces_n/ U+0020 SPACE characters into /key_n/ at
+ # random positions other than start or end of the string.
+ for _ in xrange(spaces):
+ pos = random.randint(1, len(key) - 1)
+ key = key[0:pos] + ' ' + key[pos:]
+ return number, key
+
+ def _generate_key3(self):
+ # 4.1 26. let /key3/ be a string consisting of eight random bytes (or
+ # equivalently, a random 64 bit integer encoded in a big-endian order).
+ return ''.join([chr(random.randint(0, 255)) for _ in xrange(8)])
+
+
+class ClientConnection(object):
+ """A wrapper for socket object to provide the mp_conn interface.
+ mod_pywebsocket library is designed to be working on Apache mod_python's
+ mp_conn object.
+ """
+
+ def __init__(self, socket):
+ self._socket = socket
+
+ def write(self, data):
+ self._socket.sendall(data)
+
+ def read(self, n):
+ return self._socket.recv(n)
+
+ def get_remote_addr(self):
+ return self._socket.getpeername()
+ remote_addr = property(get_remote_addr)
+
+
+class ClientRequest(object):
+ """A wrapper class just to make it able to pass a socket object to
+ functions that expect a mp_request object.
+ """
+
+ def __init__(self, socket):
+ self._logger = util.get_class_logger(self)
+
+ self._socket = socket
+ self.connection = ClientConnection(socket)
+
+
+def _import_ssl():
+ global ssl
+ try:
+ import ssl
+ return True
+ except ImportError:
+ return False
+
+
+def _import_pyopenssl():
+ global OpenSSL
+ try:
+ import OpenSSL.SSL
+ return True
+ except ImportError:
+ return False
+
+
+class EchoClient(object):
+ """WebSocket echo client."""
+
+ def __init__(self, options):
+ self._options = options
+ self._socket = None
+
+ self._logger = util.get_class_logger(self)
+
+ def run(self):
+ """Run the client.
+
+ Shake hands and then repeat sending message and receiving its echo.
+ """
+
+ self._socket = socket.socket()
+ self._socket.settimeout(self._options.socket_timeout)
+ try:
+ self._socket.connect((self._options.server_host,
+ self._options.server_port))
+ if self._options.use_tls:
+ self._socket = _TLSSocket(
+ self._socket,
+ self._options.tls_module,
+ self._options.tls_version,
+ self._options.disable_tls_compression)
+
+ version = self._options.protocol_version
+
+ if (version == _PROTOCOL_VERSION_HYBI08 or
+ version == _PROTOCOL_VERSION_HYBI13):
+ self._handshake = ClientHandshakeProcessor(
+ self._socket, self._options)
+ elif version == _PROTOCOL_VERSION_HYBI00:
+ self._handshake = ClientHandshakeProcessorHybi00(
+ self._socket, self._options)
+ else:
+ raise ValueError(
+ 'Invalid --protocol-version flag: %r' % version)
+
+ self._handshake.handshake()
+
+ self._logger.info('Connection established')
+
+ request = ClientRequest(self._socket)
+
+ version_map = {
+ _PROTOCOL_VERSION_HYBI08: common.VERSION_HYBI08,
+ _PROTOCOL_VERSION_HYBI13: common.VERSION_HYBI13,
+ _PROTOCOL_VERSION_HYBI00: common.VERSION_HYBI00}
+ request.ws_version = version_map[version]
+
+ if (version == _PROTOCOL_VERSION_HYBI08 or
+ version == _PROTOCOL_VERSION_HYBI13):
+ stream_option = StreamOptions()
+ stream_option.mask_send = True
+ stream_option.unmask_receive = False
+
+ if self._options.deflate_frame is not False:
+ processor = self._options.deflate_frame
+ processor.setup_stream_options(stream_option)
+
+ if self._options.use_permessage_deflate is not False:
+ framer = self._options.use_permessage_deflate
+ framer.setup_stream_options(stream_option)
+
+ self._stream = Stream(request, stream_option)
+ elif version == _PROTOCOL_VERSION_HYBI00:
+ self._stream = StreamHixie75(request, True)
+
+ for line in self._options.message.split(','):
+ self._stream.send_message(line)
+ if self._options.verbose:
+ print 'Send: %s' % line
+ try:
+ received = self._stream.receive_message()
+
+ if self._options.verbose:
+ print 'Recv: %s' % received
+ except Exception, e:
+ if self._options.verbose:
+ print 'Error: %s' % e
+ raise
+
+ self._do_closing_handshake()
+ finally:
+ self._socket.close()
+
+ def _do_closing_handshake(self):
+ """Perform closing handshake using the specified closing frame."""
+
+ if self._options.message.split(',')[-1] == _GOODBYE_MESSAGE:
+ # requested server initiated closing handshake, so
+ # expecting closing handshake message from server.
+ self._logger.info('Wait for server-initiated closing handshake')
+ message = self._stream.receive_message()
+ if message is None:
+ print 'Recv close'
+ print 'Send ack'
+ self._logger.info(
+ 'Received closing handshake and sent ack')
+ return
+ print 'Send close'
+ self._stream.close_connection()
+ self._logger.info('Sent closing handshake')
+ print 'Recv ack'
+ self._logger.info('Received ack')
+
+
+def main():
+ sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
+
+ parser = OptionParser()
+ # We accept --command_line_flag style flags which is the same as Google
+ # gflags in addition to common --command-line-flag style flags.
+ parser.add_option('-s', '--server-host', '--server_host',
+ dest='server_host', type='string',
+ default='localhost', help='server host')
+ parser.add_option('-p', '--server-port', '--server_port',
+ dest='server_port', type='int',
+ default=_UNDEFINED_PORT, help='server port')
+ parser.add_option('-o', '--origin', dest='origin', type='string',
+ default=None, help='origin')
+ parser.add_option('-r', '--resource', dest='resource', type='string',
+ default='/echo', help='resource path')
+ parser.add_option('-m', '--message', dest='message', type='string',
+ help=('comma-separated messages to send. '
+ '%s will force close the connection from server.' %
+ _GOODBYE_MESSAGE))
+ parser.add_option('-q', '--quiet', dest='verbose', action='store_false',
+ default=True, help='suppress messages')
+ parser.add_option('-t', '--tls', dest='use_tls', action='store_true',
+ default=False, help='use TLS (wss://). By default, '
+ 'it looks for ssl and pyOpenSSL module and uses found '
+ 'one. Use --tls-module option to specify which module '
+ 'to use')
+ parser.add_option('--tls-module', '--tls_module', dest='tls_module',
+ type='choice',
+ choices=[_TLS_BY_STANDARD_MODULE, _TLS_BY_PYOPENSSL],
+ help='Use ssl module if "%s" is specified. '
+ 'Use pyOpenSSL module if "%s" is specified' %
+ (_TLS_BY_STANDARD_MODULE, _TLS_BY_PYOPENSSL))
+ parser.add_option('--tls-version', '--tls_version',
+ dest='tls_version',
+ type='string', default=_TLS_VERSION_SSL23,
+ help='TLS/SSL version to use. One of \'' +
+ _TLS_VERSION_SSL23 + '\' (SSL version 2 or 3), \'' +
+ _TLS_VERSION_SSL3 + '\' (SSL version 3), \'' +
+ _TLS_VERSION_TLS1 + '\' (TLS version 1)')
+ parser.add_option('--disable-tls-compression', '--disable_tls_compression',
+ dest='disable_tls_compression',
+ action='store_true', default=False,
+ help='Disable TLS compression. Available only when '
+ 'pyOpenSSL module is used.')
+ parser.add_option('-k', '--socket-timeout', '--socket_timeout',
+ dest='socket_timeout', type='int', default=_TIMEOUT_SEC,
+ help='Timeout(sec) for sockets')
+ parser.add_option('--draft75', dest='draft75',
+ action='store_true', default=False,
+ help='Obsolete option. Don\'t use this.')
+ parser.add_option('--protocol-version', '--protocol_version',
+ dest='protocol_version',
+ type='string', default=_PROTOCOL_VERSION_HYBI13,
+ help='WebSocket protocol version to use. One of \'' +
+ _PROTOCOL_VERSION_HYBI13 + '\', \'' +
+ _PROTOCOL_VERSION_HYBI08 + '\', \'' +
+ _PROTOCOL_VERSION_HYBI00 + '\'')
+ parser.add_option('--version-header', '--version_header',
+ dest='version_header',
+ type='int', default=-1,
+ help='Specify Sec-WebSocket-Version header value')
+ parser.add_option('--deflate-frame', '--deflate_frame',
+ dest='deflate_frame',
+ action='store_true', default=False,
+ help='Use the deflate-frame extension.')
+ parser.add_option('--use-permessage-deflate', '--use_permessage_deflate',
+ dest='use_permessage_deflate',
+ action='store_true', default=False,
+ help='Use the permessage-deflate extension.')
+ parser.add_option('--log-level', '--log_level', type='choice',
+ dest='log_level', default='warn',
+ choices=['debug', 'info', 'warn', 'error', 'critical'],
+ help='Log level.')
+
+ (options, unused_args) = parser.parse_args()
+
+ logging.basicConfig(level=logging.getLevelName(options.log_level.upper()))
+
+ if options.draft75:
+ logging.critical('--draft75 option is obsolete.')
+ sys.exit(1)
+
+ if options.protocol_version == _PROTOCOL_VERSION_HIXIE75:
+ logging.critical(
+ 'Value %s is obsolete for --protocol_version options' %
+ _PROTOCOL_VERSION_HIXIE75)
+ sys.exit(1)
+
+ if options.use_tls:
+ if options.tls_module is None:
+ if _import_ssl():
+ options.tls_module = _TLS_BY_STANDARD_MODULE
+ logging.debug('Using ssl module')
+ elif _import_pyopenssl():
+ options.tls_module = _TLS_BY_PYOPENSSL
+ logging.debug('Using pyOpenSSL module')
+ else:
+ logging.critical(
+ 'TLS support requires ssl or pyOpenSSL module.')
+ sys.exit(1)
+ elif options.tls_module == _TLS_BY_STANDARD_MODULE:
+ if not _import_ssl():
+ logging.critical('ssl module is not available')
+ sys.exit(1)
+ elif options.tls_module == _TLS_BY_PYOPENSSL:
+ if not _import_pyopenssl():
+ logging.critical('pyOpenSSL module is not available')
+ sys.exit(1)
+ else:
+ logging.critical('Invalid --tls-module option: %r',
+ options.tls_module)
+ sys.exit(1)
+
+ if (options.disable_tls_compression and
+ options.tls_module != _TLS_BY_PYOPENSSL):
+ logging.critical('You can disable TLS compression only when '
+ 'pyOpenSSL module is used.')
+ sys.exit(1)
+ else:
+ if options.tls_module is not None:
+ logging.critical('Use --tls-module option only together with '
+ '--use-tls option.')
+ sys.exit(1)
+
+ if options.disable_tls_compression:
+ logging.critical('Use --disable-tls-compression only together '
+ 'with --use-tls option.')
+ sys.exit(1)
+
+ # Default port number depends on whether TLS is used.
+ if options.server_port == _UNDEFINED_PORT:
+ if options.use_tls:
+ options.server_port = common.DEFAULT_WEB_SOCKET_SECURE_PORT
+ else:
+ options.server_port = common.DEFAULT_WEB_SOCKET_PORT
+
+ # optparse doesn't seem to handle non-ascii default values.
+ # Set default message here.
+ if not options.message:
+ options.message = u'Hello,\u65e5\u672c' # "Japan" in Japanese
+
+ EchoClient(options).run()
+
+
+if __name__ == '__main__':
+ main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/echo_noext_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/echo_noext_wsh.py
new file mode 100644
index 000000000..1df515122
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/echo_noext_wsh.py
@@ -0,0 +1,61 @@
+# Copyright 2013, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+_GOODBYE_MESSAGE = u'Goodbye'
+
+
+def web_socket_do_extra_handshake(request):
+ """Received Sec-WebSocket-Extensions header value is parsed into
+ request.ws_requested_extensions. pywebsocket creates extension
+ processors using it before do_extra_handshake call and never looks at it
+ after the call.
+
+ To reject requested extensions, clear the processor list.
+ """
+
+ request.ws_extension_processors = []
+
+
+def web_socket_transfer_data(request):
+ """Echo. Same as echo_wsh.py."""
+
+ while True:
+ line = request.ws_stream.receive_message()
+ if line is None:
+ return
+ if isinstance(line, unicode):
+ request.ws_stream.send_message(line, binary=False)
+ if line == _GOODBYE_MESSAGE:
+ return
+ else:
+ request.ws_stream.send_message(line, binary=True)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/echo_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/echo_wsh.py
new file mode 100644
index 000000000..38646c32c
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/echo_wsh.py
@@ -0,0 +1,54 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+_GOODBYE_MESSAGE = u'Goodbye'
+
+
+def web_socket_do_extra_handshake(request):
+ # This example handler accepts any request. See origin_check_wsh.py for how
+ # to reject access from untrusted scripts based on origin value.
+
+ pass # Always accept.
+
+
+def web_socket_transfer_data(request):
+ while True:
+ line = request.ws_stream.receive_message()
+ if line is None:
+ return
+ if isinstance(line, unicode):
+ request.ws_stream.send_message(line, binary=False)
+ if line == _GOODBYE_MESSAGE:
+ return
+ else:
+ request.ws_stream.send_message(line, binary=True)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/eventsource.cgi b/testing/web-platform/tests/tools/pywebsocket/src/example/eventsource.cgi
new file mode 100755
index 000000000..adddf237c
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/eventsource.cgi
@@ -0,0 +1,54 @@
+#!/usr/bin/python
+
+# Copyright 2013, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""This CGI script generates text/event-stream type data stream for testing
+the Server-Sent Events.
+
+It will only work correctly with HTTP servers that do not buffer the output of
+CGI scripts.
+"""
+
+
+import sys
+import time
+
+sys.stdout.write('Content-type: text/event-stream\r\n\r\n')
+
+id = 0
+
+while True:
+ sys.stdout.write('data: Hello\r\nid: %d\r\n\r\n' % id)
+ sys.stdout.flush()
+
+ id = id + 1
+
+ time.sleep(1)
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/eventsource.html b/testing/web-platform/tests/tools/pywebsocket/src/example/eventsource.html
new file mode 100644
index 000000000..1598a8807
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/eventsource.html
@@ -0,0 +1,74 @@
+<!--
+Copyright 2013, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+
+<!--
+Simple example of the Server-Sent Events
+http://dev.w3.org/html5/eventsource/
+
+For comparison with the WebSocket Protocol & API.
+
+Run the pywebsocket with the --cgi_path parameter.
+-->
+
+<html>
+<head>
+<title>Server-Sent Events Example</title>
+<script>
+var eventSource = null;
+
+function addToLog(data) {
+ logBox.value += data + '\n';
+ logBox.scrollTop = 1000000;
+}
+
+function init() {
+ logBox = document.getElementById('log');
+
+ eventSource = new EventSource('/eventsource.cgi');
+ eventSource.onopen = function() {
+ addToLog('onopen (readyState = ' + eventSource.readyState + ')');
+ }
+ eventSource.onmessage = function(event) {
+ addToLog(event.data);
+ }
+ eventSource.onerror = function(event) {
+ addToLog('onerror (readyState = ' + eventSource.readyState + ')');
+ }
+}
+</script>
+</head>
+<body onload="init()">
+<textarea id="log" rows="10" cols="40" readonly></textarea>
+<p style="font-size: small">
+ Make sure that pywebsocket is run with --cgi_path parameter.
+</p>
+</body>
+</html>
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/handler_map.txt b/testing/web-platform/tests/tools/pywebsocket/src/example/handler_map.txt
new file mode 100644
index 000000000..21c4c09aa
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/handler_map.txt
@@ -0,0 +1,11 @@
+# websocket handler map file, used by standalone.py -m option.
+# A line starting with '#' is a comment line.
+# Each line consists of 'alias_resource_path' and 'existing_resource_path'
+# separated by spaces.
+# Aliasing is processed from the top to the bottom of the line, and
+# 'existing_resource_path' must exist before it is aliased.
+# For example,
+# / /echo
+# means that a request to '/' will be handled by handlers for '/echo'.
+/ /echo
+
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/hsts_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/hsts_wsh.py
new file mode 100644
index 000000000..e86194692
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/hsts_wsh.py
@@ -0,0 +1,40 @@
+# Copyright 2013, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+def web_socket_do_extra_handshake(request):
+ request.extra_headers.append(
+ ('Strict-Transport-Security', 'max-age=86400'))
+
+
+def web_socket_transfer_data(request):
+ request.ws_stream.send_message('Hello', binary=False)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/internal_error_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/internal_error_wsh.py
new file mode 100644
index 000000000..fe581b54a
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/internal_error_wsh.py
@@ -0,0 +1,42 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+from mod_pywebsocket import msgutil
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ raise msgutil.BadOperationException('Intentional')
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/origin_check_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/origin_check_wsh.py
new file mode 100644
index 000000000..e05767ab9
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/origin_check_wsh.py
@@ -0,0 +1,44 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# This example is derived from test/testdata/handlers/origin_check_wsh.py.
+
+
+def web_socket_do_extra_handshake(request):
+ if request.ws_origin == 'http://example.com':
+ return
+ raise ValueError('Unacceptable origin: %r' % request.ws_origin)
+
+
+def web_socket_transfer_data(request):
+ request.connection.write('origin_check_wsh.py is called for %s, %s' %
+ (request.ws_resource, request.ws_protocol))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/pywebsocket.conf b/testing/web-platform/tests/tools/pywebsocket/src/example/pywebsocket.conf
new file mode 100644
index 000000000..335d130a5
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/pywebsocket.conf
@@ -0,0 +1,42 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#
+# Sample configuration file for apache2
+#
+LogLevel debug
+<IfModule python_module>
+ PythonPath "sys.path+['/mod_pywebsocket']"
+ PythonOption mod_pywebsocket.handler_root /var/www
+ PythonOption mod_pywebsocket.handler_scan /var/www/ws
+ #PythonOption mod_pywebsocket.allow_draft75 On
+ <Location /ws>
+ PythonHeaderParserHandler mod_pywebsocket.headerparserhandler
+ </Location>
+</IfModule>
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/special_headers.cgi b/testing/web-platform/tests/tools/pywebsocket/src/example/special_headers.cgi
new file mode 100755
index 000000000..ea5080f1f
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/special_headers.cgi
@@ -0,0 +1,28 @@
+#!/usr/bin/python
+
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the COPYING file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""CGI script sample for testing effect of HTTP headers on the origin page.
+
+Note that CGI scripts don't work on the standalone pywebsocket running in TLS
+mode.
+"""
+
+
+print """Content-type: text/html
+Content-Security-Policy: connect-src self
+
+<html>
+<head>
+<title></title>
+</head>
+<body>
+<script>
+var socket = new WebSocket("ws://example.com");
+</script>
+</body>
+</html>"""
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/util.js b/testing/web-platform/tests/tools/pywebsocket/src/example/util.js
new file mode 100644
index 000000000..a1cad4975
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/util.js
@@ -0,0 +1,177 @@
+// Copyright 2013, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+// Utilities for example applications (for both main and worker thread).
+
+var results = {};
+
+function getTimeStamp() {
+ return Date.now();
+}
+
+function formatResultInKiB(size, timePerMessageInMs, stddevTimePerMessageInMs,
+ speed, printSize) {
+ if (printSize) {
+ return (size / 1024) +
+ '\t' + timePerMessageInMs.toFixed(3) +
+ (stddevTimePerMessageInMs == -1 ?
+ '' :
+ '\t' + stddevTimePerMessageInMs.toFixed(3)) +
+ '\t' + speed.toFixed(3);
+ } else {
+ return speed.toString();
+ }
+}
+
+function clearAverageData() {
+ results = {};
+}
+
+function reportAverageData(config) {
+ config.addToSummary(
+ 'Size[KiB]\tAverage time[ms]\tStddev time[ms]\tSpeed[KB/s]');
+ for (var size in results) {
+ var averageTimePerMessageInMs = results[size].sum_t / results[size].n;
+ var speed = calculateSpeedInKB(size, averageTimePerMessageInMs);
+ // Calculate sample standard deviation
+ var stddevTimePerMessageInMs = Math.sqrt(
+ (results[size].sum_t2 / results[size].n -
+ averageTimePerMessageInMs * averageTimePerMessageInMs) *
+ results[size].n /
+ (results[size].n - 1));
+ config.addToSummary(formatResultInKiB(
+ size, averageTimePerMessageInMs, stddevTimePerMessageInMs, speed,
+ true));
+ }
+}
+
+function calculateSpeedInKB(size, timeSpentInMs) {
+ return Math.round(size / timeSpentInMs * 1000) / 1000;
+}
+
+function calculateAndLogResult(config, size, startTimeInMs, totalSize) {
+ var timeSpentInMs = getTimeStamp() - startTimeInMs;
+ var speed = calculateSpeedInKB(totalSize, timeSpentInMs);
+ var timePerMessageInMs = timeSpentInMs / (totalSize / size);
+ if (!results[size]) {
+ results[size] = {n: 0, sum_t: 0, sum_t2: 0};
+ }
+ config.measureValue(timePerMessageInMs);
+ results[size].n ++;
+ results[size].sum_t += timePerMessageInMs;
+ results[size].sum_t2 += timePerMessageInMs * timePerMessageInMs;
+ config.addToLog(formatResultInKiB(size, timePerMessageInMs, -1, speed,
+ config.printSize));
+}
+
+function fillArrayBuffer(buffer, c) {
+ var i;
+
+ var u32Content = c * 0x01010101;
+
+ var u32Blocks = Math.floor(buffer.byteLength / 4);
+ var u32View = new Uint32Array(buffer, 0, u32Blocks);
+ // length attribute is slow on Chrome. Don't use it for loop condition.
+ for (i = 0; i < u32Blocks; ++i) {
+ u32View[i] = u32Content;
+ }
+
+ // Fraction
+ var u8Blocks = buffer.byteLength - u32Blocks * 4;
+ var u8View = new Uint8Array(buffer, u32Blocks * 4, u8Blocks);
+ for (i = 0; i < u8Blocks; ++i) {
+ u8View[i] = c;
+ }
+}
+
+function verifyArrayBuffer(buffer, expectedChar) {
+ var i;
+
+ var expectedU32Value = expectedChar * 0x01010101;
+
+ var u32Blocks = Math.floor(buffer.byteLength / 4);
+ var u32View = new Uint32Array(buffer, 0, u32Blocks);
+ for (i = 0; i < u32Blocks; ++i) {
+ if (u32View[i] != expectedU32Value) {
+ return false;
+ }
+ }
+
+ var u8Blocks = buffer.byteLength - u32Blocks * 4;
+ var u8View = new Uint8Array(buffer, u32Blocks * 4, u8Blocks);
+ for (i = 0; i < u8Blocks; ++i) {
+ if (u8View[i] != expectedChar) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function verifyBlob(config, blob, expectedChar, doneCallback) {
+ var reader = new FileReader(blob);
+ reader.onerror = function() {
+ config.addToLog('FileReader Error: ' + reader.error.message);
+ doneCallback(blob.size, false);
+ }
+ reader.onloadend = function() {
+ var result = verifyArrayBuffer(reader.result, expectedChar);
+ doneCallback(blob.size, result);
+ }
+ reader.readAsArrayBuffer(blob);
+}
+
+function verifyAcknowledgement(config, message, size) {
+ if (typeof message != 'string') {
+ config.addToLog('Invalid ack type: ' + typeof message);
+ return false;
+ }
+ var parsedAck = parseInt(message);
+ if (isNaN(parsedAck)) {
+ config.addToLog('Invalid ack value: ' + message);
+ return false;
+ }
+ if (parsedAck != size) {
+ config.addToLog(
+ 'Expected ack for ' + size + 'B but received one for ' + parsedAck +
+ 'B');
+ return false;
+ }
+
+ return true;
+}
+
+function cloneConfig(obj) {
+ var newObj = {};
+ for (key in obj) {
+ newObj[key] = obj[key];
+ }
+ return newObj;
+}
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/util_main.js b/testing/web-platform/tests/tools/pywebsocket/src/example/util_main.js
new file mode 100644
index 000000000..b03d1c2bd
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/util_main.js
@@ -0,0 +1,63 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the COPYING file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+// Utilities for example applications (for the main thread only).
+
+var logBox = null;
+var queuedLog = '';
+
+var summaryBox = null;
+
+function queueLog(log) {
+ queuedLog += log + '\n';
+}
+
+function addToLog(log) {
+ logBox.value += queuedLog;
+ queuedLog = '';
+ logBox.value += log + '\n';
+ logBox.scrollTop = 1000000;
+}
+
+function addToSummary(log) {
+ summaryBox.value += log + '\n';
+ summaryBox.scrollTop = 1000000;
+}
+
+// value: execution time in milliseconds.
+// config.measureValue is intended to be used in Performance Tests.
+// Do nothing here in non-PerformanceTest.
+function measureValue(value) {
+}
+
+function getIntFromInput(id) {
+ return parseInt(document.getElementById(id).value);
+}
+
+function getStringFromRadioBox(name) {
+ var list = document.getElementById('benchmark_form')[name];
+ for (var i = 0; i < list.length; ++i)
+ if (list.item(i).checked)
+ return list.item(i).value;
+ return undefined;
+}
+function getBoolFromCheckBox(id) {
+ return document.getElementById(id).checked;
+}
+
+function getIntArrayFromInput(id) {
+ var strArray = document.getElementById(id).value.split(',');
+ return strArray.map(function(str) { return parseInt(str, 10); });
+}
+
+function onMessage(message) {
+ if (message.data.type === 'addToLog')
+ addToLog(message.data.data);
+ else if (message.data.type === 'addToSummary')
+ addToSummary(message.data.data);
+ else if (message.data.type === 'measureValue')
+ measureValue(message.data.data);
+}
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/util_worker.js b/testing/web-platform/tests/tools/pywebsocket/src/example/util_worker.js
new file mode 100644
index 000000000..b64f7829d
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/util_worker.js
@@ -0,0 +1,19 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the COPYING file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+// Utilities for example applications (for the worker threads only).
+
+function workerAddToLog(text) {
+ postMessage({type: 'addToLog', data: text});
+}
+
+function workerAddToSummary(text) {
+ postMessage({type: 'addToSummary', data: text});
+}
+
+function workerMeasureValue(value) {
+ postMessage({type: 'measureValue', data: value});
+}
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_benchmark.html b/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_benchmark.html
new file mode 100644
index 000000000..186229775
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_benchmark.html
@@ -0,0 +1,222 @@
+<!--
+Copyright 2013, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+
+<html>
+<head>
+<title>XMLHttpRequest benchmark</title>
+<script src="util_main.js"></script>
+<script src="util.js"></script>
+<script src="xhr_benchmark.js"></script>
+<script>
+var addressBox = null;
+
+function getConfig() {
+ return {
+ prefixUrl: addressBox.value,
+ printSize: getBoolFromCheckBox('printsize'),
+ numXHRs: getIntFromInput('numXHRs'),
+ async: getBoolFromCheckBox('async'),
+ // Initial size of messages.
+ numIterations: getIntFromInput('numiterations'),
+ numWarmUpIterations: getIntFromInput('numwarmupiterations'),
+ startSize: getIntFromInput('startsize'),
+ // Stops benchmark when the size of message exceeds this threshold.
+ stopThreshold: getIntFromInput('stopthreshold'),
+ // If the size of each message is small, send/receive multiple messages
+ // until the sum of sizes reaches this threshold.
+ // minTotal: getIntFromInput('mintotal'),
+ // minTotal is not yet implemented on XHR benchmark
+ multipliers: getIntArrayFromInput('multipliers'),
+ verifyData: getBoolFromCheckBox('verifydata')
+ };
+}
+
+var worker = new Worker('xhr_benchmark.js');
+worker.onmessage = onMessage;
+
+function onSendBenchmark() {
+ var config = getConfig();
+ config.dataType = getStringFromRadioBox('datatyperadio');
+
+ if (getBoolFromCheckBox('worker')) {
+ worker.postMessage({type: 'sendBenchmark', config: config});
+ } else {
+ config.addToLog = addToLog;
+ config.addToSummary = addToSummary;
+ config.measureValue = measureValue;
+ sendBenchmark(config);
+ }
+}
+
+function onReceiveBenchmark() {
+ var config = getConfig();
+ config.dataType = getStringFromRadioBox('datatyperadio');
+
+ if (getBoolFromCheckBox('worker')) {
+ worker.postMessage({type: 'receiveBenchmark', config: config});
+ } else {
+ config.addToLog = addToLog;
+ config.addToSummary = addToSummary;
+ config.measureValue = measureValue;
+ receiveBenchmark(config);
+ }
+}
+
+function onBatchBenchmark() {
+ var config = getConfig();
+
+ if (getBoolFromCheckBox('worker')) {
+ worker.postMessage({type: 'batchBenchmark', config: config});
+ } else {
+ config.addToLog = addToLog;
+ config.addToSummary = addToSummary;
+ config.measureValue = measureValue;
+ batchBenchmark(config);
+ }
+}
+
+function onStop() {
+ var config = getConfig();
+
+ if (getBoolFromCheckBox('worker')) {
+ worker.postMessage({type: 'stop', config: config});
+ } else {
+ config.addToLog = addToLog;
+ config.addToSummary = addToSummary;
+ config.measureValue = measureValue;
+ stop(config);
+ }
+}
+
+function init() {
+ addressBox = document.getElementById('address');
+ logBox = document.getElementById('log');
+
+ summaryBox = document.getElementById('summary');
+
+ // Special address of pywebsocket for XHR benchmark.
+ addressBox.value = '/073be001e10950692ccbf3a2ad21c245';
+
+ addToLog(window.navigator.userAgent.toLowerCase());
+ addToSummary(window.navigator.userAgent.toLowerCase());
+}
+</script>
+</head>
+<body onload="init()">
+
+<form id="benchmark_form">
+ url prefix <input type="text" id="address" size="40">
+ <input type="button" value="send" onclick="onSendBenchmark()">
+ <input type="button" value="receive" onclick="onReceiveBenchmark()">
+ <input type="button" value="batch" onclick="onBatchBenchmark()">
+ <input type="button" value="stop" onclick="onStop()">
+
+ <br/>
+
+ <input type="checkbox" id="printsize" checked>
+ <label for="printsize">Print size and time per message</label>
+ <input type="checkbox" id="verifydata" checked>
+ <label for="verifydata">Verify data</label>
+ <input type="checkbox" id="worker">
+ <label for="worker">Run on worker</label>
+ <input type="checkbox" id="async" checked>
+ <label for="async">Async</label><br>
+ (Receive &amp;&amp; Non-Worker &amp;&amp; Sync is not supported by spec)
+
+ <br/>
+
+ Parameters:
+
+ <br/>
+
+ <table>
+ <tr>
+ <td>Num XHRs</td>
+ <td><input type="text" id="numXHRs" value="1"></td>
+ </tr>
+ <tr>
+ <td>Number of iterations</td>
+ <td><input type="text" id="numiterations" value="1"></td>
+ </tr>
+ <tr>
+ <td>Number of warm-up iterations</td>
+ <td><input type="text" id="numwarmupiterations" value="0"></td>
+ </tr>
+ <tr>
+ <td>Start size</td>
+ <td><input type="text" id="startsize" value="10240"></td>
+ </tr>
+ <tr>
+ <td>Stop threshold</td>
+ <td><input type="text" id="stopthreshold" value="102400000"></td>
+ </tr>
+ <tr>
+ <td>Minimum total</td>
+ <td><input type="text" id="mintotal" value="102400000"></td>
+ </tr>
+ <tr>
+ <td>Multipliers</td>
+ <td><input type="text" id="multipliers" value="5, 2"></td>
+ </tr>
+ </table>
+
+ Set data type
+ <input type="radio"
+ name="datatyperadio"
+ id="datatyperadiotext"
+ value="text"
+ checked><label for="datatyperadiotext">text</label>
+ <input type="radio"
+ name="datatyperadio"
+ id="datatyperadioblob"
+ value="blob"
+ ><label for="datatyperadioblob">blob</label>
+ <input type="radio"
+ name="datatyperadio"
+ id="datatyperadioarraybuffer"
+ value="arraybuffer"
+ ><label for="datatyperadioarraybuffer">arraybuffer</label>
+</form>
+
+<div id="log_div">
+ <textarea
+ id="log" rows="20" style="width: 100%" readonly></textarea>
+</div>
+<div id="summary_div">
+ Summary
+ <textarea
+ id="summary" rows="20" style="width: 100%" readonly></textarea>
+</div>
+
+Note: Effect of RTT and time spent for ArrayBuffer creation in receive benchmarks are not eliminated.
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_benchmark.js b/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_benchmark.js
new file mode 100644
index 000000000..233c7cb38
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_benchmark.js
@@ -0,0 +1,389 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the COPYING file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+var isWorker = typeof importScripts !== "undefined";
+
+if (isWorker) {
+ // Running on a worker
+ importScripts('util.js', 'util_worker.js');
+}
+
+// Namespace for holding globals.
+var benchmark = {};
+benchmark.startTimeInMs = 0;
+
+var xhrs = [];
+
+var timerID = null;
+
+function destroyAllXHRs() {
+ for (var i = 0; i < xhrs.length; ++i) {
+ xhrs[i].onreadystatechange = null;
+ // Abort XHRs if they are not yet DONE state.
+ // Calling abort() here (i.e. in onreadystatechange handler)
+ // causes "NetworkError" messages in DevTools in sync mode,
+ // even if it is after transition to DONE state.
+ if (xhrs[i].readyState != XMLHttpRequest.DONE)
+ xhrs[i].abort();
+ }
+ xhrs = [];
+ // gc() might be needed for Chrome/Blob
+}
+
+function repeatString(str, count) {
+ var data = '';
+ var expChunk = str;
+ var remain = count;
+ while (true) {
+ if (remain % 2) {
+ data += expChunk;
+ remain = (remain - 1) / 2;
+ } else {
+ remain /= 2;
+ }
+
+ if (remain == 0)
+ break;
+
+ expChunk = expChunk + expChunk;
+ }
+ return data;
+}
+
+function sendBenchmarkStep(size, config) {
+ timerID = null;
+
+ benchmark.startTimeInMs = null;
+ var totalSize = 0;
+ var totalReplied = 0;
+
+ var onReadyStateChangeHandler = function () {
+ if (this.readyState != this.DONE) {
+ return;
+ }
+
+ if (this.status != 200) {
+ config.addToLog('Failed (status=' + this.status + ')');
+ destroyAllXHRs();
+ return;
+ }
+
+ if (config.verifyData &&
+ !verifyAcknowledgement(config, this.response, size)) {
+ destroyAllXHRs();
+ return;
+ }
+
+ totalReplied += size;
+
+ if (totalReplied < totalSize) {
+ return;
+ }
+
+ if (benchmark.startTimeInMs == null) {
+ config.addToLog('startTimeInMs not set');
+ destroyAllXHRs();
+ return;
+ }
+
+ calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize);
+
+ destroyAllXHRs();
+
+ runNextTask(config);
+ };
+
+ for (var i = 0; i < config.numXHRs; ++i) {
+ var xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = onReadyStateChangeHandler;
+ xhrs.push(xhr);
+ }
+
+ var dataArray = [];
+
+ for (var i = 0; i < xhrs.length; ++i) {
+ var data = null;
+ if (config.dataType == 'arraybuffer' ||
+ config.dataType == 'blob') {
+ data = new ArrayBuffer(size);
+
+ fillArrayBuffer(data, 0x61);
+
+ if (config.dataType == 'blob') {
+ data = new Blob([data]);
+ }
+ } else {
+ data = repeatString('a', size);
+ }
+
+ dataArray.push(data);
+ }
+
+
+ benchmark.startTimeInMs = getTimeStamp();
+ totalSize = size * xhrs.length;
+
+ for (var i = 0; i < xhrs.length; ++i) {
+ var data = dataArray[i];
+ var xhr = xhrs[i];
+ xhr.open('POST', config.prefixUrl + '_send', config.async);
+ xhr.send(data);
+ }
+}
+
+function receiveBenchmarkStep(size, config) {
+ timerID = null;
+
+ benchmark.startTimeInMs = null;
+ var totalSize = 0;
+ var totalReplied = 0;
+
+ var checkResultAndContinue = function (bytesReceived, verificationResult) {
+ if (!verificationResult) {
+ config.addToLog('Response verification failed');
+ destroyAllXHRs();
+ return;
+ }
+
+ totalReplied += bytesReceived;
+
+ if (totalReplied < totalSize) {
+ return;
+ }
+
+ if (benchmark.startTimeInMs == null) {
+ config.addToLog('startTimeInMs not set');
+ destroyAllXHRs();
+ return;
+ }
+
+ calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize);
+
+ destroyAllXHRs();
+
+ runNextTask(config);
+ }
+
+ var onReadyStateChangeHandler = function () {
+ if (this.readyState != this.DONE) {
+ return;
+ }
+
+ if (this.status != 200) {
+ config.addToLog('Failed (status=' + this.status + ')');
+ destroyAllXHRs();
+ return;
+ }
+
+ var bytesReceived = -1;
+ if (this.responseType == 'arraybuffer') {
+ bytesReceived = this.response.byteLength;
+ } else if (this.responseType == 'blob') {
+ bytesReceived = this.response.size;
+ } else {
+ bytesReceived = this.response.length;
+ }
+ if (bytesReceived != size) {
+ config.addToLog('Expected ' + size +
+ 'B but received ' + bytesReceived + 'B');
+ destroyAllXHRs();
+ return;
+ }
+
+ if (this.responseType == 'arraybuffer') {
+ checkResultAndContinue(bytesReceived,
+ !config.verifyData || verifyArrayBuffer(this.response, 0x61));
+ } else if (this.responseType == 'blob') {
+ if (config.verifyData)
+ verifyBlob(config, this.response, 0x61, checkResultAndContinue);
+ else
+ checkResultAndContinue(bytesReceived, true);
+ } else {
+ checkResultAndContinue(
+ bytesReceived,
+ !config.verifyData ||
+ this.response == repeatString('a', this.response.length));
+ }
+ };
+
+ for (var i = 0; i < config.numXHRs; ++i) {
+ var xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = onReadyStateChangeHandler;
+ xhrs.push(xhr);
+ }
+
+ benchmark.startTimeInMs = getTimeStamp();
+ totalSize = size * xhrs.length;
+
+ for (var i = 0; i < xhrs.length; ++i) {
+ var xhr = xhrs[i];
+ xhr.open('POST', config.prefixUrl + '_receive', config.async);
+ xhr.responseType = config.dataType;
+ xhr.send(size + ' none');
+ }
+}
+
+
+function getConfigString(config) {
+ return '(' + config.dataType +
+ ', verifyData=' + config.verifyData +
+ ', ' + (isWorker ? 'Worker' : 'Main') +
+ ', ' + (config.async ? 'Async' : 'Sync') +
+ ', numXHRs=' + config.numXHRs +
+ ', numIterations=' + config.numIterations +
+ ', numWarmUpIterations=' + config.numWarmUpIterations +
+ ')';
+}
+
+function startBenchmark(config) {
+ clearTimeout(timerID);
+ destroyAllXHRs();
+
+ runNextTask(config);
+}
+
+// TODO(hiroshige): the following code is the same as benchmark.html
+// and some of them should be merged into e.g. util.js
+
+var tasks = [];
+
+function runNextTask(config) {
+ var task = tasks.shift();
+ if (task == undefined) {
+ config.addToLog('Finished');
+ destroyAllXHRs();
+ return;
+ }
+ timerID = setTimeout(task, 0);
+}
+
+function buildLegendString(config) {
+ var legend = ''
+ if (config.printSize)
+ legend = 'Message size in KiB, Time/message in ms, ';
+ legend += 'Speed in kB/s';
+ return legend;
+}
+
+function addTasks(config, stepFunc) {
+ for (var i = 0;
+ i < config.numWarmUpIterations + config.numIterations; ++i) {
+ // Ignore the first |config.numWarmUpIterations| iterations.
+ if (i == config.numWarmUpIterations)
+ addResultClearingTask(config);
+
+ var multiplierIndex = 0;
+ for (var size = config.startSize;
+ size <= config.stopThreshold;
+ ++multiplierIndex) {
+ var task = stepFunc.bind(
+ null,
+ size,
+ config);
+ tasks.push(task);
+ size *= config.multipliers[
+ multiplierIndex % config.multipliers.length];
+ }
+ }
+}
+
+function addResultReportingTask(config, title) {
+ tasks.push(function(){
+ timerID = null;
+ config.addToSummary(title);
+ reportAverageData(config);
+ clearAverageData();
+ runNextTask(config);
+ });
+}
+
+function addResultClearingTask(config) {
+ tasks.push(function(){
+ timerID = null;
+ clearAverageData();
+ runNextTask(config);
+ });
+}
+
+// --------------------------------
+
+function sendBenchmark(config) {
+ config.addToLog('Send benchmark');
+ config.addToLog(buildLegendString(config));
+
+ tasks = [];
+ clearAverageData();
+ addTasks(config, sendBenchmarkStep);
+ addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config));
+ startBenchmark(config);
+}
+
+function receiveBenchmark(config) {
+ config.addToLog('Receive benchmark');
+ config.addToLog(buildLegendString(config));
+
+ tasks = [];
+ clearAverageData();
+ addTasks(config, receiveBenchmarkStep);
+ addResultReportingTask(config,
+ 'Receive Benchmark ' + getConfigString(config));
+ startBenchmark(config);
+}
+
+function batchBenchmark(originalConfig) {
+ originalConfig.addToLog('Batch benchmark');
+
+ tasks = [];
+ clearAverageData();
+
+ var dataTypes = ['text', 'blob', 'arraybuffer'];
+ var stepFuncs = [sendBenchmarkStep, receiveBenchmarkStep];
+ var names = ['Send', 'Receive'];
+ var async = [true, false];
+ for (var i = 0; i < stepFuncs.length; ++i) {
+ for (var j = 0; j < dataTypes.length; ++j) {
+ for (var k = 0; k < async.length; ++k) {
+ var config = cloneConfig(originalConfig);
+ config.dataType = dataTypes[j];
+ config.async = async[k];
+
+ // Receive && Non-Worker && Sync is not supported by the spec
+ if (stepFuncs[i] === receiveBenchmarkStep && !isWorker &&
+ !config.async)
+ continue;
+
+ addTasks(config, stepFuncs[i]);
+ addResultReportingTask(config,
+ names[i] + ' benchmark ' + getConfigString(config));
+ }
+ }
+ }
+
+ startBenchmark(config);
+}
+
+
+function stop(config) {
+ destroyAllXHRs();
+ clearTimeout(timerID);
+ timerID = null;
+ config.addToLog('Stopped');
+}
+
+onmessage = function (message) {
+ var config = message.data.config;
+ config.addToLog = workerAddToLog;
+ config.addToSummary = workerAddToSummary;
+ config.measureValue = workerMeasureValue;
+ if (message.data.type === 'sendBenchmark')
+ sendBenchmark(config);
+ else if (message.data.type === 'receiveBenchmark')
+ receiveBenchmark(config);
+ else if (message.data.type === 'batchBenchmark')
+ batchBenchmark(config);
+ else if (message.data.type === 'stop')
+ stop(config);
+};
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_event_logger.html b/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_event_logger.html
new file mode 100644
index 000000000..6983553b8
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_event_logger.html
@@ -0,0 +1,110 @@
+<!--
+Copyright 2014 Google Inc. All rights reserved.
+
+Use of this source code is governed by a BSD-style
+license that can be found in the COPYING file or at
+https://developers.google.com/open-source/licenses/bsd
+-->
+
+<html>
+<head>
+<title>XHR event logger</title>
+<script src="util_main.js"></script>
+<script>
+var events = [];
+
+function run() {
+ events = [];
+
+ function pushToLog(type) {
+ if (events.length != 0 && type === events[events.length - 1].type) {
+ events[events.length - 1].count += 1;
+ } else {
+ events.push({type: type, count: 1});
+ }
+ }
+
+ var xhr = new XMLHttpRequest();
+
+ function getProgressEventDump(e) {
+ return '(' + e.lengthComputable + ', ' + e.loaded + ', ' + e.total + ')';
+ }
+
+ var dumpProgressEvent = getBoolFromCheckBox('dumpprogressevent');
+
+ function log(e) {
+ var type = e.type;
+ if (type === 'readystatechange') {
+ type += e.target.readyState;
+ }
+ if (dumpProgressEvent && (e instanceof ProgressEvent)) {
+ type += getProgressEventDump(e);
+ }
+ pushToLog(type);
+ };
+
+ function logUpload(e) {
+ var type = e.type;
+ if (dumpProgressEvent && (e instanceof ProgressEvent)) {
+ type += getProgressEventDump(e);
+ }
+ pushToLog('upload' + type);
+ }
+
+ if (getBoolFromCheckBox('upload')) {
+ var upload = xhr.upload;
+ upload.onloadstart = logUpload;
+ upload.onprogress = logUpload;
+ upload.onabort = logUpload;
+ upload.onerror = logUpload;
+ upload.onload = logUpload;
+ upload.ontimeout = logUpload;
+ upload.onloadend = logUpload;
+ }
+
+ xhr.onreadystatechange = log;
+ xhr.onloadstart = log;
+ xhr.onprogress = log;
+ xhr.onabort = log;
+ xhr.onerror = log;
+ xhr.onload = log;
+ xhr.ontimeout = log;
+ xhr.onloadend = log;
+
+ xhr.open('POST', '/073be001e10950692ccbf3a2ad21c245_receive',
+ getBoolFromCheckBox('async'));
+ var size = getIntFromInput('size');
+ var chunkedMode = 'none';
+ if (getBoolFromCheckBox('chunkedresponse')) {
+ chunkedMode = 'chunked';
+ }
+ xhr.send(size + ' ' + chunkedMode);
+}
+
+function print() {
+ var result = '';
+ for (var i = 0; i < events.length; ++i) {
+ var event = events[i];
+ result += event.type + ' * ' + event.count + '\n';
+ }
+ document.getElementById('log').value = result;
+}
+</script>
+
+<body>
+ <textarea id="log" rows="10" cols="40" readonly></textarea>
+ <br/>
+ Size: <input type="text" id="size" value="65536"><br/>
+ <input type="checkbox" id="chunkedresponse">
+ <label for="chunkedresponse">Use Chunked T-E for response</label><br/>
+ <input type="checkbox" id="upload">
+ <label for="upload">Upload progress</label><br/>
+ <input type="checkbox" id="dumpprogressevent">
+ <label for="dumpprogressevent">
+ Dump lengthComputable/loaded/total</label><br/>
+ <input type="checkbox" id="async" checked>
+ <label for="async">Async</label><br/>
+ <input type="button" onclick="run()" value="Run XHR">
+ <input type="button" onclick="print()" value="Print log">
+</body>
+</html>
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/__init__.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/__init__.py
new file mode 100644
index 000000000..70933a220
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/__init__.py
@@ -0,0 +1,224 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""WebSocket extension for Apache HTTP Server.
+
+mod_pywebsocket is a WebSocket extension for Apache HTTP Server
+intended for testing or experimental purposes. mod_python is required.
+
+
+Installation
+============
+
+0. Prepare an Apache HTTP Server for which mod_python is enabled.
+
+1. Specify the following Apache HTTP Server directives to suit your
+ configuration.
+
+ If mod_pywebsocket is not in the Python path, specify the following.
+ <websock_lib> is the directory where mod_pywebsocket is installed.
+
+ PythonPath "sys.path+['<websock_lib>']"
+
+ Always specify the following. <websock_handlers> is the directory where
+ user-written WebSocket handlers are placed.
+
+ PythonOption mod_pywebsocket.handler_root <websock_handlers>
+ PythonHeaderParserHandler mod_pywebsocket.headerparserhandler
+
+ To limit the search for WebSocket handlers to a directory <scan_dir>
+ under <websock_handlers>, configure as follows:
+
+ PythonOption mod_pywebsocket.handler_scan <scan_dir>
+
+ <scan_dir> is useful in saving scan time when <websock_handlers>
+ contains many non-WebSocket handler files.
+
+ If you want to allow handlers whose canonical path is not under the root
+ directory (i.e. symbolic link is in root directory but its target is not),
+ configure as follows:
+
+ PythonOption mod_pywebsocket.allow_handlers_outside_root_dir On
+
+ Example snippet of httpd.conf:
+ (mod_pywebsocket is in /websock_lib, WebSocket handlers are in
+ /websock_handlers, port is 80 for ws, 443 for wss.)
+
+ <IfModule python_module>
+ PythonPath "sys.path+['/websock_lib']"
+ PythonOption mod_pywebsocket.handler_root /websock_handlers
+ PythonHeaderParserHandler mod_pywebsocket.headerparserhandler
+ </IfModule>
+
+2. Tune Apache parameters for serving WebSocket. We'd like to note that at
+ least TimeOut directive from core features and RequestReadTimeout
+ directive from mod_reqtimeout should be modified not to kill connections
+ in only a few seconds of idle time.
+
+3. Verify installation. You can use example/console.html to poke the server.
+
+
+Writing WebSocket handlers
+==========================
+
+When a WebSocket request comes in, the resource name
+specified in the handshake is considered as if it is a file path under
+<websock_handlers> and the handler defined in
+<websock_handlers>/<resource_name>_wsh.py is invoked.
+
+For example, if the resource name is /example/chat, the handler defined in
+<websock_handlers>/example/chat_wsh.py is invoked.
+
+A WebSocket handler is composed of the following three functions:
+
+ web_socket_do_extra_handshake(request)
+ web_socket_transfer_data(request)
+ web_socket_passive_closing_handshake(request)
+
+where:
+ request: mod_python request.
+
+web_socket_do_extra_handshake is called during the handshake after the
+headers are successfully parsed and WebSocket properties (ws_location,
+ws_origin, and ws_resource) are added to request. A handler
+can reject the request by raising an exception.
+
+A request object has the following properties that you can use during the
+extra handshake (web_socket_do_extra_handshake):
+- ws_resource
+- ws_origin
+- ws_version
+- ws_location (HyBi 00 only)
+- ws_extensions (HyBi 06 and later)
+- ws_deflate (HyBi 06 and later)
+- ws_protocol
+- ws_requested_protocols (HyBi 06 and later)
+
+The last two are a bit tricky. See the next subsection.
+
+
+Subprotocol Negotiation
+-----------------------
+
+For HyBi 06 and later, ws_protocol is always set to None when
+web_socket_do_extra_handshake is called. If ws_requested_protocols is not
+None, you must choose one subprotocol from this list and set it to
+ws_protocol.
+
+For HyBi 00, when web_socket_do_extra_handshake is called,
+ws_protocol is set to the value given by the client in
+Sec-WebSocket-Protocol header or None if
+such header was not found in the opening handshake request. Finish extra
+handshake with ws_protocol untouched to accept the request subprotocol.
+Then, Sec-WebSocket-Protocol header will be sent to
+the client in response with the same value as requested. Raise an exception
+in web_socket_do_extra_handshake to reject the requested subprotocol.
+
+
+Data Transfer
+-------------
+
+web_socket_transfer_data is called after the handshake completed
+successfully. A handler can receive/send messages from/to the client
+using request. mod_pywebsocket.msgutil module provides utilities
+for data transfer.
+
+You can receive a message by the following statement.
+
+ message = request.ws_stream.receive_message()
+
+This call blocks until any complete text frame arrives, and the payload data
+of the incoming frame will be stored into message. When you're using IETF
+HyBi 00 or later protocol, receive_message() will return None on receiving
+client-initiated closing handshake. When any error occurs, receive_message()
+will raise some exception.
+
+You can send a message by the following statement.
+
+ request.ws_stream.send_message(message)
+
+
+Closing Connection
+------------------
+
+Executing the following statement or just return-ing from
+web_socket_transfer_data cause connection close.
+
+ request.ws_stream.close_connection()
+
+close_connection will wait
+for closing handshake acknowledgement coming from the client. When it
+couldn't receive a valid acknowledgement, raises an exception.
+
+web_socket_passive_closing_handshake is called after the server receives
+incoming closing frame from the client peer immediately. You can specify
+code and reason by return values. They are sent as a outgoing closing frame
+from the server. A request object has the following properties that you can
+use in web_socket_passive_closing_handshake.
+- ws_close_code
+- ws_close_reason
+
+
+Threading
+---------
+
+A WebSocket handler must be thread-safe if the server (Apache or
+standalone.py) is configured to use threads.
+
+
+Configuring WebSocket Extension Processors
+------------------------------------------
+
+See extensions.py for supported WebSocket extensions. Note that they are
+unstable and their APIs are subject to change substantially.
+
+A request object has these extension processing related attributes.
+
+- ws_requested_extensions:
+
+ A list of common.ExtensionParameter instances representing extension
+ parameters received from the client in the client's opening handshake.
+ You shouldn't modify it manually.
+
+- ws_extensions:
+
+ A list of common.ExtensionParameter instances representing extension
+ parameters to send back to the client in the server's opening handshake.
+ You shouldn't touch it directly. Instead, call methods on extension
+ processors.
+
+- ws_extension_processors:
+
+ A list of loaded extension processors. Find the processor for the
+ extension you want to configure from it, and call its methods.
+"""
+
+
+# vi:sts=4 sw=4 et tw=72
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_base.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_base.py
new file mode 100644
index 000000000..8235666bb
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_base.py
@@ -0,0 +1,181 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Base stream class.
+"""
+
+
+# Note: request.connection.write/read are used in this module, even though
+# mod_python document says that they should be used only in connection
+# handlers. Unfortunately, we have no other options. For example,
+# request.write/read are not suitable because they don't allow direct raw bytes
+# writing/reading.
+
+
+import socket
+
+from mod_pywebsocket import util
+
+
+# Exceptions
+
+
+class ConnectionTerminatedException(Exception):
+ """This exception will be raised when a connection is terminated
+ unexpectedly.
+ """
+
+ pass
+
+
+class InvalidFrameException(ConnectionTerminatedException):
+ """This exception will be raised when we received an invalid frame we
+ cannot parse.
+ """
+
+ pass
+
+
+class BadOperationException(Exception):
+ """This exception will be raised when send_message() is called on
+ server-terminated connection or receive_message() is called on
+ client-terminated connection.
+ """
+
+ pass
+
+
+class UnsupportedFrameException(Exception):
+ """This exception will be raised when we receive a frame with flag, opcode
+ we cannot handle. Handlers can just catch and ignore this exception and
+ call receive_message() again to continue processing the next frame.
+ """
+
+ pass
+
+
+class InvalidUTF8Exception(Exception):
+ """This exception will be raised when we receive a text frame which
+ contains invalid UTF-8 strings.
+ """
+
+ pass
+
+
+class StreamBase(object):
+ """Base stream class."""
+
+ def __init__(self, request):
+ """Construct an instance.
+
+ Args:
+ request: mod_python request.
+ """
+
+ self._logger = util.get_class_logger(self)
+
+ self._request = request
+
+ def _read(self, length):
+ """Reads length bytes from connection. In case we catch any exception,
+ prepends remote address to the exception message and raise again.
+
+ Raises:
+ ConnectionTerminatedException: when read returns empty string.
+ """
+
+ try:
+ read_bytes = self._request.connection.read(length)
+ if not read_bytes:
+ raise ConnectionTerminatedException(
+ 'Receiving %d byte failed. Peer (%r) closed connection' %
+ (length, (self._request.connection.remote_addr,)))
+ return read_bytes
+ except socket.error, e:
+ # Catch a socket.error. Because it's not a child class of the
+ # IOError prior to Python 2.6, we cannot omit this except clause.
+ # Use %s rather than %r for the exception to use human friendly
+ # format.
+ raise ConnectionTerminatedException(
+ 'Receiving %d byte failed. socket.error (%s) occurred' %
+ (length, e))
+ except IOError, e:
+ # Also catch an IOError because mod_python throws it.
+ raise ConnectionTerminatedException(
+ 'Receiving %d byte failed. IOError (%s) occurred' %
+ (length, e))
+
+ def _write(self, bytes_to_write):
+ """Writes given bytes to connection. In case we catch any exception,
+ prepends remote address to the exception message and raise again.
+ """
+
+ try:
+ self._request.connection.write(bytes_to_write)
+ except Exception, e:
+ util.prepend_message_to_exception(
+ 'Failed to send message to %r: ' %
+ (self._request.connection.remote_addr,),
+ e)
+ raise
+
+ def receive_bytes(self, length):
+ """Receives multiple bytes. Retries read when we couldn't receive the
+ specified amount.
+
+ Raises:
+ ConnectionTerminatedException: when read returns empty string.
+ """
+
+ read_bytes = []
+ while length > 0:
+ new_read_bytes = self._read(length)
+ read_bytes.append(new_read_bytes)
+ length -= len(new_read_bytes)
+ return ''.join(read_bytes)
+
+ def _read_until(self, delim_char):
+ """Reads bytes until we encounter delim_char. The result will not
+ contain delim_char.
+
+ Raises:
+ ConnectionTerminatedException: when read returns empty string.
+ """
+
+ read_bytes = []
+ while True:
+ ch = self._read(1)
+ if ch == delim_char:
+ break
+ read_bytes.append(ch)
+ return ''.join(read_bytes)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_hixie75.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_hixie75.py
new file mode 100644
index 000000000..94cf5b31b
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_hixie75.py
@@ -0,0 +1,229 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""This file provides a class for parsing/building frames of the WebSocket
+protocol version HyBi 00 and Hixie 75.
+
+Specification:
+- HyBi 00 http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00
+- Hixie 75 http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
+"""
+
+
+from mod_pywebsocket import common
+from mod_pywebsocket._stream_base import BadOperationException
+from mod_pywebsocket._stream_base import ConnectionTerminatedException
+from mod_pywebsocket._stream_base import InvalidFrameException
+from mod_pywebsocket._stream_base import StreamBase
+from mod_pywebsocket._stream_base import UnsupportedFrameException
+from mod_pywebsocket import util
+
+
+class StreamHixie75(StreamBase):
+ """A class for parsing/building frames of the WebSocket protocol version
+ HyBi 00 and Hixie 75.
+ """
+
+ def __init__(self, request, enable_closing_handshake=False):
+ """Construct an instance.
+
+ Args:
+ request: mod_python request.
+ enable_closing_handshake: to let StreamHixie75 perform closing
+ handshake as specified in HyBi 00, set
+ this option to True.
+ """
+
+ StreamBase.__init__(self, request)
+
+ self._logger = util.get_class_logger(self)
+
+ self._enable_closing_handshake = enable_closing_handshake
+
+ self._request.client_terminated = False
+ self._request.server_terminated = False
+
+ def send_message(self, message, end=True, binary=False):
+ """Send message.
+
+ Args:
+ message: unicode string to send.
+ binary: not used in hixie75.
+
+ Raises:
+ BadOperationException: when called on a server-terminated
+ connection.
+ """
+
+ if not end:
+ raise BadOperationException(
+ 'StreamHixie75 doesn\'t support send_message with end=False')
+
+ if binary:
+ raise BadOperationException(
+ 'StreamHixie75 doesn\'t support send_message with binary=True')
+
+ if self._request.server_terminated:
+ raise BadOperationException(
+ 'Requested send_message after sending out a closing handshake')
+
+ self._write(''.join(['\x00', message.encode('utf-8'), '\xff']))
+
+ def _read_payload_length_hixie75(self):
+ """Reads a length header in a Hixie75 version frame with length.
+
+ Raises:
+ ConnectionTerminatedException: when read returns empty string.
+ """
+
+ length = 0
+ while True:
+ b_str = self._read(1)
+ b = ord(b_str)
+ length = length * 128 + (b & 0x7f)
+ if (b & 0x80) == 0:
+ break
+ return length
+
+ def receive_message(self):
+ """Receive a WebSocket frame and return its payload an unicode string.
+
+ Returns:
+ payload unicode string in a WebSocket frame.
+
+ Raises:
+ ConnectionTerminatedException: when read returns empty
+ string.
+ BadOperationException: when called on a client-terminated
+ connection.
+ """
+
+ if self._request.client_terminated:
+ raise BadOperationException(
+ 'Requested receive_message after receiving a closing '
+ 'handshake')
+
+ while True:
+ # Read 1 byte.
+ # mp_conn.read will block if no bytes are available.
+ # Timeout is controlled by TimeOut directive of Apache.
+ frame_type_str = self.receive_bytes(1)
+ frame_type = ord(frame_type_str)
+ if (frame_type & 0x80) == 0x80:
+ # The payload length is specified in the frame.
+ # Read and discard.
+ length = self._read_payload_length_hixie75()
+ if length > 0:
+ _ = self.receive_bytes(length)
+ # 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the
+ # /client terminated/ flag and abort these steps.
+ if not self._enable_closing_handshake:
+ continue
+
+ if frame_type == 0xFF and length == 0:
+ self._request.client_terminated = True
+
+ if self._request.server_terminated:
+ self._logger.debug(
+ 'Received ack for server-initiated closing '
+ 'handshake')
+ return None
+
+ self._logger.debug(
+ 'Received client-initiated closing handshake')
+
+ self._send_closing_handshake()
+ self._logger.debug(
+ 'Sent ack for client-initiated closing handshake')
+ return None
+ else:
+ # The payload is delimited with \xff.
+ bytes = self._read_until('\xff')
+ # The WebSocket protocol section 4.4 specifies that invalid
+ # characters must be replaced with U+fffd REPLACEMENT
+ # CHARACTER.
+ message = bytes.decode('utf-8', 'replace')
+ if frame_type == 0x00:
+ return message
+ # Discard data of other types.
+
+ def _send_closing_handshake(self):
+ if not self._enable_closing_handshake:
+ raise BadOperationException(
+ 'Closing handshake is not supported in Hixie 75 protocol')
+
+ self._request.server_terminated = True
+
+ # 5.3 the server may decide to terminate the WebSocket connection by
+ # running through the following steps:
+ # 1. send a 0xFF byte and a 0x00 byte to the client to indicate the
+ # start of the closing handshake.
+ self._write('\xff\x00')
+
+ def close_connection(self, unused_code='', unused_reason=''):
+ """Closes a WebSocket connection.
+
+ Raises:
+ ConnectionTerminatedException: when closing handshake was
+ not successfull.
+ """
+
+ if self._request.server_terminated:
+ self._logger.debug(
+ 'Requested close_connection but server is already terminated')
+ return
+
+ if not self._enable_closing_handshake:
+ self._request.server_terminated = True
+ self._logger.debug('Connection closed')
+ return
+
+ self._send_closing_handshake()
+ self._logger.debug('Sent server-initiated closing handshake')
+
+ # TODO(ukai): 2. wait until the /client terminated/ flag has been set,
+ # or until a server-defined timeout expires.
+ #
+ # For now, we expect receiving closing handshake right after sending
+ # out closing handshake, and if we couldn't receive non-handshake
+ # frame, we take it as ConnectionTerminatedException.
+ message = self.receive_message()
+ if message is not None:
+ raise ConnectionTerminatedException(
+ 'Didn\'t receive valid ack for closing handshake')
+ # TODO: 3. close the WebSocket connection.
+ # note: mod_python Connection (mp_conn) doesn't have close method.
+
+ def send_ping(self, body):
+ raise BadOperationException(
+ 'StreamHixie75 doesn\'t support send_ping')
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_hybi.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_hybi.py
new file mode 100644
index 000000000..a8a49e3c3
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_hybi.py
@@ -0,0 +1,887 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""This file provides classes and helper functions for parsing/building frames
+of the WebSocket protocol (RFC 6455).
+
+Specification:
+http://tools.ietf.org/html/rfc6455
+"""
+
+
+from collections import deque
+import logging
+import os
+import struct
+import time
+
+from mod_pywebsocket import common
+from mod_pywebsocket import util
+from mod_pywebsocket._stream_base import BadOperationException
+from mod_pywebsocket._stream_base import ConnectionTerminatedException
+from mod_pywebsocket._stream_base import InvalidFrameException
+from mod_pywebsocket._stream_base import InvalidUTF8Exception
+from mod_pywebsocket._stream_base import StreamBase
+from mod_pywebsocket._stream_base import UnsupportedFrameException
+
+
+_NOOP_MASKER = util.NoopMasker()
+
+
+class Frame(object):
+
+ def __init__(self, fin=1, rsv1=0, rsv2=0, rsv3=0,
+ opcode=None, payload=''):
+ self.fin = fin
+ self.rsv1 = rsv1
+ self.rsv2 = rsv2
+ self.rsv3 = rsv3
+ self.opcode = opcode
+ self.payload = payload
+
+
+# Helper functions made public to be used for writing unittests for WebSocket
+# clients.
+
+
+def create_length_header(length, mask):
+ """Creates a length header.
+
+ Args:
+ length: Frame length. Must be less than 2^63.
+ mask: Mask bit. Must be boolean.
+
+ Raises:
+ ValueError: when bad data is given.
+ """
+
+ if mask:
+ mask_bit = 1 << 7
+ else:
+ mask_bit = 0
+
+ if length < 0:
+ raise ValueError('length must be non negative integer')
+ elif length <= 125:
+ return chr(mask_bit | length)
+ elif length < (1 << 16):
+ return chr(mask_bit | 126) + struct.pack('!H', length)
+ elif length < (1 << 63):
+ return chr(mask_bit | 127) + struct.pack('!Q', length)
+ else:
+ raise ValueError('Payload is too big for one frame')
+
+
+def create_header(opcode, payload_length, fin, rsv1, rsv2, rsv3, mask):
+ """Creates a frame header.
+
+ Raises:
+ Exception: when bad data is given.
+ """
+
+ if opcode < 0 or 0xf < opcode:
+ raise ValueError('Opcode out of range')
+
+ if payload_length < 0 or (1 << 63) <= payload_length:
+ raise ValueError('payload_length out of range')
+
+ if (fin | rsv1 | rsv2 | rsv3) & ~1:
+ raise ValueError('FIN bit and Reserved bit parameter must be 0 or 1')
+
+ header = ''
+
+ first_byte = ((fin << 7)
+ | (rsv1 << 6) | (rsv2 << 5) | (rsv3 << 4)
+ | opcode)
+ header += chr(first_byte)
+ header += create_length_header(payload_length, mask)
+
+ return header
+
+
+def _build_frame(header, body, mask):
+ if not mask:
+ return header + body
+
+ masking_nonce = os.urandom(4)
+ masker = util.RepeatedXorMasker(masking_nonce)
+
+ return header + masking_nonce + masker.mask(body)
+
+
+def _filter_and_format_frame_object(frame, mask, frame_filters):
+ for frame_filter in frame_filters:
+ frame_filter.filter(frame)
+
+ header = create_header(
+ frame.opcode, len(frame.payload), frame.fin,
+ frame.rsv1, frame.rsv2, frame.rsv3, mask)
+ return _build_frame(header, frame.payload, mask)
+
+
+def create_binary_frame(
+ message, opcode=common.OPCODE_BINARY, fin=1, mask=False, frame_filters=[]):
+ """Creates a simple binary frame with no extension, reserved bit."""
+
+ frame = Frame(fin=fin, opcode=opcode, payload=message)
+ return _filter_and_format_frame_object(frame, mask, frame_filters)
+
+
+def create_text_frame(
+ message, opcode=common.OPCODE_TEXT, fin=1, mask=False, frame_filters=[]):
+ """Creates a simple text frame with no extension, reserved bit."""
+
+ encoded_message = message.encode('utf-8')
+ return create_binary_frame(encoded_message, opcode, fin, mask,
+ frame_filters)
+
+
+def parse_frame(receive_bytes, logger=None,
+ ws_version=common.VERSION_HYBI_LATEST,
+ unmask_receive=True):
+ """Parses a frame. Returns a tuple containing each header field and
+ payload.
+
+ Args:
+ receive_bytes: a function that reads frame data from a stream or
+ something similar. The function takes length of the bytes to be
+ read. The function must raise ConnectionTerminatedException if
+ there is not enough data to be read.
+ logger: a logging object.
+ ws_version: the version of WebSocket protocol.
+ unmask_receive: unmask received frames. When received unmasked
+ frame, raises InvalidFrameException.
+
+ Raises:
+ ConnectionTerminatedException: when receive_bytes raises it.
+ InvalidFrameException: when the frame contains invalid data.
+ """
+
+ if not logger:
+ logger = logging.getLogger()
+
+ logger.log(common.LOGLEVEL_FINE, 'Receive the first 2 octets of a frame')
+
+ received = receive_bytes(2)
+
+ first_byte = ord(received[0])
+ fin = (first_byte >> 7) & 1
+ rsv1 = (first_byte >> 6) & 1
+ rsv2 = (first_byte >> 5) & 1
+ rsv3 = (first_byte >> 4) & 1
+ opcode = first_byte & 0xf
+
+ second_byte = ord(received[1])
+ mask = (second_byte >> 7) & 1
+ payload_length = second_byte & 0x7f
+
+ logger.log(common.LOGLEVEL_FINE,
+ 'FIN=%s, RSV1=%s, RSV2=%s, RSV3=%s, opcode=%s, '
+ 'Mask=%s, Payload_length=%s',
+ fin, rsv1, rsv2, rsv3, opcode, mask, payload_length)
+
+ if (mask == 1) != unmask_receive:
+ raise InvalidFrameException(
+ 'Mask bit on the received frame did\'nt match masking '
+ 'configuration for received frames')
+
+ # The HyBi and later specs disallow putting a value in 0x0-0xFFFF
+ # into the 8-octet extended payload length field (or 0x0-0xFD in
+ # 2-octet field).
+ valid_length_encoding = True
+ length_encoding_bytes = 1
+ if payload_length == 127:
+ logger.log(common.LOGLEVEL_FINE,
+ 'Receive 8-octet extended payload length')
+
+ extended_payload_length = receive_bytes(8)
+ payload_length = struct.unpack(
+ '!Q', extended_payload_length)[0]
+ if payload_length > 0x7FFFFFFFFFFFFFFF:
+ raise InvalidFrameException(
+ 'Extended payload length >= 2^63')
+ if ws_version >= 13 and payload_length < 0x10000:
+ valid_length_encoding = False
+ length_encoding_bytes = 8
+
+ logger.log(common.LOGLEVEL_FINE,
+ 'Decoded_payload_length=%s', payload_length)
+ elif payload_length == 126:
+ logger.log(common.LOGLEVEL_FINE,
+ 'Receive 2-octet extended payload length')
+
+ extended_payload_length = receive_bytes(2)
+ payload_length = struct.unpack(
+ '!H', extended_payload_length)[0]
+ if ws_version >= 13 and payload_length < 126:
+ valid_length_encoding = False
+ length_encoding_bytes = 2
+
+ logger.log(common.LOGLEVEL_FINE,
+ 'Decoded_payload_length=%s', payload_length)
+
+ if not valid_length_encoding:
+ logger.warning(
+ 'Payload length is not encoded using the minimal number of '
+ 'bytes (%d is encoded using %d bytes)',
+ payload_length,
+ length_encoding_bytes)
+
+ if mask == 1:
+ logger.log(common.LOGLEVEL_FINE, 'Receive mask')
+
+ masking_nonce = receive_bytes(4)
+ masker = util.RepeatedXorMasker(masking_nonce)
+
+ logger.log(common.LOGLEVEL_FINE, 'Mask=%r', masking_nonce)
+ else:
+ masker = _NOOP_MASKER
+
+ logger.log(common.LOGLEVEL_FINE, 'Receive payload data')
+ if logger.isEnabledFor(common.LOGLEVEL_FINE):
+ receive_start = time.time()
+
+ raw_payload_bytes = receive_bytes(payload_length)
+
+ if logger.isEnabledFor(common.LOGLEVEL_FINE):
+ logger.log(
+ common.LOGLEVEL_FINE,
+ 'Done receiving payload data at %s MB/s',
+ payload_length / (time.time() - receive_start) / 1000 / 1000)
+ logger.log(common.LOGLEVEL_FINE, 'Unmask payload data')
+
+ if logger.isEnabledFor(common.LOGLEVEL_FINE):
+ unmask_start = time.time()
+
+ unmasked_bytes = masker.mask(raw_payload_bytes)
+
+ if logger.isEnabledFor(common.LOGLEVEL_FINE):
+ logger.log(
+ common.LOGLEVEL_FINE,
+ 'Done unmasking payload data at %s MB/s',
+ payload_length / (time.time() - unmask_start) / 1000 / 1000)
+
+ return opcode, unmasked_bytes, fin, rsv1, rsv2, rsv3
+
+
+class FragmentedFrameBuilder(object):
+ """A stateful class to send a message as fragments."""
+
+ def __init__(self, mask, frame_filters=[], encode_utf8=True):
+ """Constructs an instance."""
+
+ self._mask = mask
+ self._frame_filters = frame_filters
+ # This is for skipping UTF-8 encoding when building text type frames
+ # from compressed data.
+ self._encode_utf8 = encode_utf8
+
+ self._started = False
+
+ # Hold opcode of the first frame in messages to verify types of other
+ # frames in the message are all the same.
+ self._opcode = common.OPCODE_TEXT
+
+ def build(self, payload_data, end, binary):
+ if binary:
+ frame_type = common.OPCODE_BINARY
+ else:
+ frame_type = common.OPCODE_TEXT
+ if self._started:
+ if self._opcode != frame_type:
+ raise ValueError('Message types are different in frames for '
+ 'the same message')
+ opcode = common.OPCODE_CONTINUATION
+ else:
+ opcode = frame_type
+ self._opcode = frame_type
+
+ if end:
+ self._started = False
+ fin = 1
+ else:
+ self._started = True
+ fin = 0
+
+ if binary or not self._encode_utf8:
+ return create_binary_frame(
+ payload_data, opcode, fin, self._mask, self._frame_filters)
+ else:
+ return create_text_frame(
+ payload_data, opcode, fin, self._mask, self._frame_filters)
+
+
+def _create_control_frame(opcode, body, mask, frame_filters):
+ frame = Frame(opcode=opcode, payload=body)
+
+ for frame_filter in frame_filters:
+ frame_filter.filter(frame)
+
+ if len(frame.payload) > 125:
+ raise BadOperationException(
+ 'Payload data size of control frames must be 125 bytes or less')
+
+ header = create_header(
+ frame.opcode, len(frame.payload), frame.fin,
+ frame.rsv1, frame.rsv2, frame.rsv3, mask)
+ return _build_frame(header, frame.payload, mask)
+
+
+def create_ping_frame(body, mask=False, frame_filters=[]):
+ return _create_control_frame(common.OPCODE_PING, body, mask, frame_filters)
+
+
+def create_pong_frame(body, mask=False, frame_filters=[]):
+ return _create_control_frame(common.OPCODE_PONG, body, mask, frame_filters)
+
+
+def create_close_frame(body, mask=False, frame_filters=[]):
+ return _create_control_frame(
+ common.OPCODE_CLOSE, body, mask, frame_filters)
+
+
+def create_closing_handshake_body(code, reason):
+ body = ''
+ if code is not None:
+ if (code > common.STATUS_USER_PRIVATE_MAX or
+ code < common.STATUS_NORMAL_CLOSURE):
+ raise BadOperationException('Status code is out of range')
+ if (code == common.STATUS_NO_STATUS_RECEIVED or
+ code == common.STATUS_ABNORMAL_CLOSURE or
+ code == common.STATUS_TLS_HANDSHAKE):
+ raise BadOperationException('Status code is reserved pseudo '
+ 'code')
+ encoded_reason = reason.encode('utf-8')
+ body = struct.pack('!H', code) + encoded_reason
+ return body
+
+
+class StreamOptions(object):
+ """Holds option values to configure Stream objects."""
+
+ def __init__(self):
+ """Constructs StreamOptions."""
+
+ # Filters applied to frames.
+ self.outgoing_frame_filters = []
+ self.incoming_frame_filters = []
+
+ # Filters applied to messages. Control frames are not affected by them.
+ self.outgoing_message_filters = []
+ self.incoming_message_filters = []
+
+ self.encode_text_message_to_utf8 = True
+ self.mask_send = False
+ self.unmask_receive = True
+
+
+class Stream(StreamBase):
+ """A class for parsing/building frames of the WebSocket protocol
+ (RFC 6455).
+ """
+
+ def __init__(self, request, options):
+ """Constructs an instance.
+
+ Args:
+ request: mod_python request.
+ """
+
+ StreamBase.__init__(self, request)
+
+ self._logger = util.get_class_logger(self)
+
+ self._options = options
+
+ self._request.client_terminated = False
+ self._request.server_terminated = False
+
+ # Holds body of received fragments.
+ self._received_fragments = []
+ # Holds the opcode of the first fragment.
+ self._original_opcode = None
+
+ self._writer = FragmentedFrameBuilder(
+ self._options.mask_send, self._options.outgoing_frame_filters,
+ self._options.encode_text_message_to_utf8)
+
+ self._ping_queue = deque()
+
+ def _receive_frame(self):
+ """Receives a frame and return data in the frame as a tuple containing
+ each header field and payload separately.
+
+ Raises:
+ ConnectionTerminatedException: when read returns empty
+ string.
+ InvalidFrameException: when the frame contains invalid data.
+ """
+
+ def _receive_bytes(length):
+ return self.receive_bytes(length)
+
+ return parse_frame(receive_bytes=_receive_bytes,
+ logger=self._logger,
+ ws_version=self._request.ws_version,
+ unmask_receive=self._options.unmask_receive)
+
+ def _receive_frame_as_frame_object(self):
+ opcode, unmasked_bytes, fin, rsv1, rsv2, rsv3 = self._receive_frame()
+
+ return Frame(fin=fin, rsv1=rsv1, rsv2=rsv2, rsv3=rsv3,
+ opcode=opcode, payload=unmasked_bytes)
+
+ def receive_filtered_frame(self):
+ """Receives a frame and applies frame filters and message filters.
+ The frame to be received must satisfy following conditions:
+ - The frame is not fragmented.
+ - The opcode of the frame is TEXT or BINARY.
+
+ DO NOT USE this method except for testing purpose.
+ """
+
+ frame = self._receive_frame_as_frame_object()
+ if not frame.fin:
+ raise InvalidFrameException(
+ 'Segmented frames must not be received via '
+ 'receive_filtered_frame()')
+ if (frame.opcode != common.OPCODE_TEXT and
+ frame.opcode != common.OPCODE_BINARY):
+ raise InvalidFrameException(
+ 'Control frames must not be received via '
+ 'receive_filtered_frame()')
+
+ for frame_filter in self._options.incoming_frame_filters:
+ frame_filter.filter(frame)
+ for message_filter in self._options.incoming_message_filters:
+ frame.payload = message_filter.filter(frame.payload)
+ return frame
+
+ def send_message(self, message, end=True, binary=False):
+ """Send message.
+
+ Args:
+ message: text in unicode or binary in str to send.
+ binary: send message as binary frame.
+
+ Raises:
+ BadOperationException: when called on a server-terminated
+ connection or called with inconsistent message type or
+ binary parameter.
+ """
+
+ if self._request.server_terminated:
+ raise BadOperationException(
+ 'Requested send_message after sending out a closing handshake')
+
+ if binary and isinstance(message, unicode):
+ raise BadOperationException(
+ 'Message for binary frame must be instance of str')
+
+ for message_filter in self._options.outgoing_message_filters:
+ message = message_filter.filter(message, end, binary)
+
+ try:
+ # Set this to any positive integer to limit maximum size of data in
+ # payload data of each frame.
+ MAX_PAYLOAD_DATA_SIZE = -1
+
+ if MAX_PAYLOAD_DATA_SIZE <= 0:
+ self._write(self._writer.build(message, end, binary))
+ return
+
+ bytes_written = 0
+ while True:
+ end_for_this_frame = end
+ bytes_to_write = len(message) - bytes_written
+ if (MAX_PAYLOAD_DATA_SIZE > 0 and
+ bytes_to_write > MAX_PAYLOAD_DATA_SIZE):
+ end_for_this_frame = False
+ bytes_to_write = MAX_PAYLOAD_DATA_SIZE
+
+ frame = self._writer.build(
+ message[bytes_written:bytes_written + bytes_to_write],
+ end_for_this_frame,
+ binary)
+ self._write(frame)
+
+ bytes_written += bytes_to_write
+
+ # This if must be placed here (the end of while block) so that
+ # at least one frame is sent.
+ if len(message) <= bytes_written:
+ break
+ except ValueError, e:
+ raise BadOperationException(e)
+
+ def _get_message_from_frame(self, frame):
+ """Gets a message from frame. If the message is composed of fragmented
+ frames and the frame is not the last fragmented frame, this method
+ returns None. The whole message will be returned when the last
+ fragmented frame is passed to this method.
+
+ Raises:
+ InvalidFrameException: when the frame doesn't match defragmentation
+ context, or the frame contains invalid data.
+ """
+
+ if frame.opcode == common.OPCODE_CONTINUATION:
+ if not self._received_fragments:
+ if frame.fin:
+ raise InvalidFrameException(
+ 'Received a termination frame but fragmentation '
+ 'not started')
+ else:
+ raise InvalidFrameException(
+ 'Received an intermediate frame but '
+ 'fragmentation not started')
+
+ if frame.fin:
+ # End of fragmentation frame
+ self._received_fragments.append(frame.payload)
+ message = ''.join(self._received_fragments)
+ self._received_fragments = []
+ return message
+ else:
+ # Intermediate frame
+ self._received_fragments.append(frame.payload)
+ return None
+ else:
+ if self._received_fragments:
+ if frame.fin:
+ raise InvalidFrameException(
+ 'Received an unfragmented frame without '
+ 'terminating existing fragmentation')
+ else:
+ raise InvalidFrameException(
+ 'New fragmentation started without terminating '
+ 'existing fragmentation')
+
+ if frame.fin:
+ # Unfragmented frame
+
+ self._original_opcode = frame.opcode
+ return frame.payload
+ else:
+ # Start of fragmentation frame
+
+ if common.is_control_opcode(frame.opcode):
+ raise InvalidFrameException(
+ 'Control frames must not be fragmented')
+
+ self._original_opcode = frame.opcode
+ self._received_fragments.append(frame.payload)
+ return None
+
+ def _process_close_message(self, message):
+ """Processes close message.
+
+ Args:
+ message: close message.
+
+ Raises:
+ InvalidFrameException: when the message is invalid.
+ """
+
+ self._request.client_terminated = True
+
+ # Status code is optional. We can have status reason only if we
+ # have status code. Status reason can be empty string. So,
+ # allowed cases are
+ # - no application data: no code no reason
+ # - 2 octet of application data: has code but no reason
+ # - 3 or more octet of application data: both code and reason
+ if len(message) == 0:
+ self._logger.debug('Received close frame (empty body)')
+ self._request.ws_close_code = (
+ common.STATUS_NO_STATUS_RECEIVED)
+ elif len(message) == 1:
+ raise InvalidFrameException(
+ 'If a close frame has status code, the length of '
+ 'status code must be 2 octet')
+ elif len(message) >= 2:
+ self._request.ws_close_code = struct.unpack(
+ '!H', message[0:2])[0]
+ self._request.ws_close_reason = message[2:].decode(
+ 'utf-8', 'replace')
+ self._logger.debug(
+ 'Received close frame (code=%d, reason=%r)',
+ self._request.ws_close_code,
+ self._request.ws_close_reason)
+
+ # As we've received a close frame, no more data is coming over the
+ # socket. We can now safely close the socket without worrying about
+ # RST sending.
+
+ if self._request.server_terminated:
+ self._logger.debug(
+ 'Received ack for server-initiated closing handshake')
+ return
+
+ self._logger.debug(
+ 'Received client-initiated closing handshake')
+
+ code = common.STATUS_NORMAL_CLOSURE
+ reason = ''
+ if hasattr(self._request, '_dispatcher'):
+ dispatcher = self._request._dispatcher
+ code, reason = dispatcher.passive_closing_handshake(
+ self._request)
+ if code is None and reason is not None and len(reason) > 0:
+ self._logger.warning(
+ 'Handler specified reason despite code being None')
+ reason = ''
+ if reason is None:
+ reason = ''
+ self._send_closing_handshake(code, reason)
+ self._logger.debug(
+ 'Acknowledged closing handshake initiated by the peer '
+ '(code=%r, reason=%r)', code, reason)
+
+ def _process_ping_message(self, message):
+ """Processes ping message.
+
+ Args:
+ message: ping message.
+ """
+
+ try:
+ handler = self._request.on_ping_handler
+ if handler:
+ handler(self._request, message)
+ return
+ except AttributeError, e:
+ pass
+ self._send_pong(message)
+
+ def _process_pong_message(self, message):
+ """Processes pong message.
+
+ Args:
+ message: pong message.
+ """
+
+ # TODO(tyoshino): Add ping timeout handling.
+
+ inflight_pings = deque()
+
+ while True:
+ try:
+ expected_body = self._ping_queue.popleft()
+ if expected_body == message:
+ # inflight_pings contains pings ignored by the
+ # other peer. Just forget them.
+ self._logger.debug(
+ 'Ping %r is acked (%d pings were ignored)',
+ expected_body, len(inflight_pings))
+ break
+ else:
+ inflight_pings.append(expected_body)
+ except IndexError, e:
+ # The received pong was unsolicited pong. Keep the
+ # ping queue as is.
+ self._ping_queue = inflight_pings
+ self._logger.debug('Received a unsolicited pong')
+ break
+
+ try:
+ handler = self._request.on_pong_handler
+ if handler:
+ handler(self._request, message)
+ except AttributeError, e:
+ pass
+
+ def receive_message(self):
+ """Receive a WebSocket frame and return its payload as a text in
+ unicode or a binary in str.
+
+ Returns:
+ payload data of the frame
+ - as unicode instance if received text frame
+ - as str instance if received binary frame
+ or None iff received closing handshake.
+ Raises:
+ BadOperationException: when called on a client-terminated
+ connection.
+ ConnectionTerminatedException: when read returns empty
+ string.
+ InvalidFrameException: when the frame contains invalid
+ data.
+ UnsupportedFrameException: when the received frame has
+ flags, opcode we cannot handle. You can ignore this
+ exception and continue receiving the next frame.
+ """
+
+ if self._request.client_terminated:
+ raise BadOperationException(
+ 'Requested receive_message after receiving a closing '
+ 'handshake')
+
+ while True:
+ # mp_conn.read will block if no bytes are available.
+ # Timeout is controlled by TimeOut directive of Apache.
+
+ frame = self._receive_frame_as_frame_object()
+
+ # Check the constraint on the payload size for control frames
+ # before extension processes the frame.
+ # See also http://tools.ietf.org/html/rfc6455#section-5.5
+ if (common.is_control_opcode(frame.opcode) and
+ len(frame.payload) > 125):
+ raise InvalidFrameException(
+ 'Payload data size of control frames must be 125 bytes or '
+ 'less')
+
+ for frame_filter in self._options.incoming_frame_filters:
+ frame_filter.filter(frame)
+
+ if frame.rsv1 or frame.rsv2 or frame.rsv3:
+ raise UnsupportedFrameException(
+ 'Unsupported flag is set (rsv = %d%d%d)' %
+ (frame.rsv1, frame.rsv2, frame.rsv3))
+
+ message = self._get_message_from_frame(frame)
+ if message is None:
+ continue
+
+ for message_filter in self._options.incoming_message_filters:
+ message = message_filter.filter(message)
+
+ if self._original_opcode == common.OPCODE_TEXT:
+ # The WebSocket protocol section 4.4 specifies that invalid
+ # characters must be replaced with U+fffd REPLACEMENT
+ # CHARACTER.
+ try:
+ return message.decode('utf-8')
+ except UnicodeDecodeError, e:
+ raise InvalidUTF8Exception(e)
+ elif self._original_opcode == common.OPCODE_BINARY:
+ return message
+ elif self._original_opcode == common.OPCODE_CLOSE:
+ self._process_close_message(message)
+ return None
+ elif self._original_opcode == common.OPCODE_PING:
+ self._process_ping_message(message)
+ elif self._original_opcode == common.OPCODE_PONG:
+ self._process_pong_message(message)
+ else:
+ raise UnsupportedFrameException(
+ 'Opcode %d is not supported' % self._original_opcode)
+
+ def _send_closing_handshake(self, code, reason):
+ body = create_closing_handshake_body(code, reason)
+ frame = create_close_frame(
+ body, mask=self._options.mask_send,
+ frame_filters=self._options.outgoing_frame_filters)
+
+ self._request.server_terminated = True
+
+ self._write(frame)
+
+ def close_connection(self, code=common.STATUS_NORMAL_CLOSURE, reason='',
+ wait_response=True):
+ """Closes a WebSocket connection.
+
+ Args:
+ code: Status code for close frame. If code is None, a close
+ frame with empty body will be sent.
+ reason: string representing close reason.
+ wait_response: True when caller want to wait the response.
+ Raises:
+ BadOperationException: when reason is specified with code None
+ or reason is not an instance of both str and unicode.
+ """
+
+ if self._request.server_terminated:
+ self._logger.debug(
+ 'Requested close_connection but server is already terminated')
+ return
+
+ if code is None:
+ if reason is not None and len(reason) > 0:
+ raise BadOperationException(
+ 'close reason must not be specified if code is None')
+ reason = ''
+ else:
+ if not isinstance(reason, str) and not isinstance(reason, unicode):
+ raise BadOperationException(
+ 'close reason must be an instance of str or unicode')
+
+ self._send_closing_handshake(code, reason)
+ self._logger.debug(
+ 'Initiated closing handshake (code=%r, reason=%r)',
+ code, reason)
+
+ if (code == common.STATUS_GOING_AWAY or
+ code == common.STATUS_PROTOCOL_ERROR) or not wait_response:
+ # It doesn't make sense to wait for a close frame if the reason is
+ # protocol error or that the server is going away. For some of
+ # other reasons, it might not make sense to wait for a close frame,
+ # but it's not clear, yet.
+ return
+
+ # TODO(ukai): 2. wait until the /client terminated/ flag has been set,
+ # or until a server-defined timeout expires.
+ #
+ # For now, we expect receiving closing handshake right after sending
+ # out closing handshake.
+ message = self.receive_message()
+ if message is not None:
+ raise ConnectionTerminatedException(
+ 'Didn\'t receive valid ack for closing handshake')
+ # TODO: 3. close the WebSocket connection.
+ # note: mod_python Connection (mp_conn) doesn't have close method.
+
+ def send_ping(self, body=''):
+ frame = create_ping_frame(
+ body,
+ self._options.mask_send,
+ self._options.outgoing_frame_filters)
+ self._write(frame)
+
+ self._ping_queue.append(body)
+
+ def _send_pong(self, body):
+ frame = create_pong_frame(
+ body,
+ self._options.mask_send,
+ self._options.outgoing_frame_filters)
+ self._write(frame)
+
+ def get_last_received_opcode(self):
+ """Returns the opcode of the WebSocket message which the last received
+ frame belongs to. The return value is valid iff immediately after
+ receive_message call.
+ """
+
+ return self._original_opcode
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/common.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/common.py
new file mode 100644
index 000000000..2fc2ead64
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/common.py
@@ -0,0 +1,303 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""This file must not depend on any module specific to the WebSocket protocol.
+"""
+
+
+from mod_pywebsocket import http_header_util
+
+
+# Additional log level definitions.
+LOGLEVEL_FINE = 9
+
+# Constants indicating WebSocket protocol version.
+VERSION_HIXIE75 = -1
+VERSION_HYBI00 = 0
+VERSION_HYBI01 = 1
+VERSION_HYBI02 = 2
+VERSION_HYBI03 = 2
+VERSION_HYBI04 = 4
+VERSION_HYBI05 = 5
+VERSION_HYBI06 = 6
+VERSION_HYBI07 = 7
+VERSION_HYBI08 = 8
+VERSION_HYBI09 = 8
+VERSION_HYBI10 = 8
+VERSION_HYBI11 = 8
+VERSION_HYBI12 = 8
+VERSION_HYBI13 = 13
+VERSION_HYBI14 = 13
+VERSION_HYBI15 = 13
+VERSION_HYBI16 = 13
+VERSION_HYBI17 = 13
+
+# Constants indicating WebSocket protocol latest version.
+VERSION_HYBI_LATEST = VERSION_HYBI13
+
+# Port numbers
+DEFAULT_WEB_SOCKET_PORT = 80
+DEFAULT_WEB_SOCKET_SECURE_PORT = 443
+
+# Schemes
+WEB_SOCKET_SCHEME = 'ws'
+WEB_SOCKET_SECURE_SCHEME = 'wss'
+
+# Frame opcodes defined in the spec.
+OPCODE_CONTINUATION = 0x0
+OPCODE_TEXT = 0x1
+OPCODE_BINARY = 0x2
+OPCODE_CLOSE = 0x8
+OPCODE_PING = 0x9
+OPCODE_PONG = 0xa
+
+# UUIDs used by HyBi 04 and later opening handshake and frame masking.
+WEBSOCKET_ACCEPT_UUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
+
+# Opening handshake header names and expected values.
+UPGRADE_HEADER = 'Upgrade'
+WEBSOCKET_UPGRADE_TYPE = 'websocket'
+WEBSOCKET_UPGRADE_TYPE_HIXIE75 = 'WebSocket'
+CONNECTION_HEADER = 'Connection'
+UPGRADE_CONNECTION_TYPE = 'Upgrade'
+HOST_HEADER = 'Host'
+ORIGIN_HEADER = 'Origin'
+SEC_WEBSOCKET_ORIGIN_HEADER = 'Sec-WebSocket-Origin'
+SEC_WEBSOCKET_KEY_HEADER = 'Sec-WebSocket-Key'
+SEC_WEBSOCKET_ACCEPT_HEADER = 'Sec-WebSocket-Accept'
+SEC_WEBSOCKET_VERSION_HEADER = 'Sec-WebSocket-Version'
+SEC_WEBSOCKET_PROTOCOL_HEADER = 'Sec-WebSocket-Protocol'
+SEC_WEBSOCKET_EXTENSIONS_HEADER = 'Sec-WebSocket-Extensions'
+SEC_WEBSOCKET_DRAFT_HEADER = 'Sec-WebSocket-Draft'
+SEC_WEBSOCKET_KEY1_HEADER = 'Sec-WebSocket-Key1'
+SEC_WEBSOCKET_KEY2_HEADER = 'Sec-WebSocket-Key2'
+SEC_WEBSOCKET_LOCATION_HEADER = 'Sec-WebSocket-Location'
+
+# Extensions
+DEFLATE_FRAME_EXTENSION = 'deflate-frame'
+PERMESSAGE_COMPRESSION_EXTENSION = 'permessage-compress'
+PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate'
+X_WEBKIT_DEFLATE_FRAME_EXTENSION = 'x-webkit-deflate-frame'
+X_WEBKIT_PERMESSAGE_COMPRESSION_EXTENSION = 'x-webkit-permessage-compress'
+MUX_EXTENSION = 'mux_DO_NOT_USE'
+
+# Status codes
+# Code STATUS_NO_STATUS_RECEIVED, STATUS_ABNORMAL_CLOSURE, and
+# STATUS_TLS_HANDSHAKE are pseudo codes to indicate specific error cases.
+# Could not be used for codes in actual closing frames.
+# Application level errors must use codes in the range
+# STATUS_USER_REGISTERED_BASE to STATUS_USER_PRIVATE_MAX. The codes in the
+# range STATUS_USER_REGISTERED_BASE to STATUS_USER_REGISTERED_MAX are managed
+# by IANA. Usually application must define user protocol level errors in the
+# range STATUS_USER_PRIVATE_BASE to STATUS_USER_PRIVATE_MAX.
+STATUS_NORMAL_CLOSURE = 1000
+STATUS_GOING_AWAY = 1001
+STATUS_PROTOCOL_ERROR = 1002
+STATUS_UNSUPPORTED_DATA = 1003
+STATUS_NO_STATUS_RECEIVED = 1005
+STATUS_ABNORMAL_CLOSURE = 1006
+STATUS_INVALID_FRAME_PAYLOAD_DATA = 1007
+STATUS_POLICY_VIOLATION = 1008
+STATUS_MESSAGE_TOO_BIG = 1009
+STATUS_MANDATORY_EXTENSION = 1010
+STATUS_INTERNAL_ENDPOINT_ERROR = 1011
+STATUS_TLS_HANDSHAKE = 1015
+STATUS_USER_REGISTERED_BASE = 3000
+STATUS_USER_REGISTERED_MAX = 3999
+STATUS_USER_PRIVATE_BASE = 4000
+STATUS_USER_PRIVATE_MAX = 4999
+# Following definitions are aliases to keep compatibility. Applications must
+# not use these obsoleted definitions anymore.
+STATUS_NORMAL = STATUS_NORMAL_CLOSURE
+STATUS_UNSUPPORTED = STATUS_UNSUPPORTED_DATA
+STATUS_CODE_NOT_AVAILABLE = STATUS_NO_STATUS_RECEIVED
+STATUS_ABNORMAL_CLOSE = STATUS_ABNORMAL_CLOSURE
+STATUS_INVALID_FRAME_PAYLOAD = STATUS_INVALID_FRAME_PAYLOAD_DATA
+STATUS_MANDATORY_EXT = STATUS_MANDATORY_EXTENSION
+
+# HTTP status codes
+HTTP_STATUS_BAD_REQUEST = 400
+HTTP_STATUS_FORBIDDEN = 403
+HTTP_STATUS_NOT_FOUND = 404
+
+
+def is_control_opcode(opcode):
+ return (opcode >> 3) == 1
+
+
+class ExtensionParameter(object):
+ """Holds information about an extension which is exchanged on extension
+ negotiation in opening handshake.
+ """
+
+ def __init__(self, name):
+ self._name = name
+ # TODO(tyoshino): Change the data structure to more efficient one such
+ # as dict when the spec changes to say like
+ # - Parameter names must be unique
+ # - The order of parameters is not significant
+ self._parameters = []
+
+ def name(self):
+ return self._name
+
+ def add_parameter(self, name, value):
+ self._parameters.append((name, value))
+
+ def get_parameters(self):
+ return self._parameters
+
+ def get_parameter_names(self):
+ return [name for name, unused_value in self._parameters]
+
+ def has_parameter(self, name):
+ for param_name, param_value in self._parameters:
+ if param_name == name:
+ return True
+ return False
+
+ def get_parameter_value(self, name):
+ for param_name, param_value in self._parameters:
+ if param_name == name:
+ return param_value
+
+
+class ExtensionParsingException(Exception):
+ def __init__(self, name):
+ super(ExtensionParsingException, self).__init__(name)
+
+
+def _parse_extension_param(state, definition):
+ param_name = http_header_util.consume_token(state)
+
+ if param_name is None:
+ raise ExtensionParsingException('No valid parameter name found')
+
+ http_header_util.consume_lwses(state)
+
+ if not http_header_util.consume_string(state, '='):
+ definition.add_parameter(param_name, None)
+ return
+
+ http_header_util.consume_lwses(state)
+
+ # TODO(tyoshino): Add code to validate that parsed param_value is token
+ param_value = http_header_util.consume_token_or_quoted_string(state)
+ if param_value is None:
+ raise ExtensionParsingException(
+ 'No valid parameter value found on the right-hand side of '
+ 'parameter %r' % param_name)
+
+ definition.add_parameter(param_name, param_value)
+
+
+def _parse_extension(state):
+ extension_token = http_header_util.consume_token(state)
+ if extension_token is None:
+ return None
+
+ extension = ExtensionParameter(extension_token)
+
+ while True:
+ http_header_util.consume_lwses(state)
+
+ if not http_header_util.consume_string(state, ';'):
+ break
+
+ http_header_util.consume_lwses(state)
+
+ try:
+ _parse_extension_param(state, extension)
+ except ExtensionParsingException, e:
+ raise ExtensionParsingException(
+ 'Failed to parse parameter for %r (%r)' %
+ (extension_token, e))
+
+ return extension
+
+
+def parse_extensions(data):
+ """Parses Sec-WebSocket-Extensions header value returns a list of
+ ExtensionParameter objects.
+
+ Leading LWSes must be trimmed.
+ """
+
+ state = http_header_util.ParsingState(data)
+
+ extension_list = []
+ while True:
+ extension = _parse_extension(state)
+ if extension is not None:
+ extension_list.append(extension)
+
+ http_header_util.consume_lwses(state)
+
+ if http_header_util.peek(state) is None:
+ break
+
+ if not http_header_util.consume_string(state, ','):
+ raise ExtensionParsingException(
+ 'Failed to parse Sec-WebSocket-Extensions header: '
+ 'Expected a comma but found %r' %
+ http_header_util.peek(state))
+
+ http_header_util.consume_lwses(state)
+
+ if len(extension_list) == 0:
+ raise ExtensionParsingException(
+ 'No valid extension entry found')
+
+ return extension_list
+
+
+def format_extension(extension):
+ """Formats an ExtensionParameter object."""
+
+ formatted_params = [extension.name()]
+ for param_name, param_value in extension.get_parameters():
+ if param_value is None:
+ formatted_params.append(param_name)
+ else:
+ quoted_value = http_header_util.quote_if_necessary(param_value)
+ formatted_params.append('%s=%s' % (param_name, quoted_value))
+ return '; '.join(formatted_params)
+
+
+def format_extensions(extension_list):
+ """Formats a list of ExtensionParameter objects."""
+
+ formatted_extension_list = []
+ for extension in extension_list:
+ formatted_extension_list.append(format_extension(extension))
+ return ', '.join(formatted_extension_list)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/dispatch.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/dispatch.py
new file mode 100644
index 000000000..96c91e0c9
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/dispatch.py
@@ -0,0 +1,393 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Dispatch WebSocket request.
+"""
+
+
+import logging
+import os
+import re
+
+from mod_pywebsocket import common
+from mod_pywebsocket import handshake
+from mod_pywebsocket import msgutil
+from mod_pywebsocket import mux
+from mod_pywebsocket import stream
+from mod_pywebsocket import util
+
+
+_SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$')
+_SOURCE_SUFFIX = '_wsh.py'
+_DO_EXTRA_HANDSHAKE_HANDLER_NAME = 'web_socket_do_extra_handshake'
+_TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data'
+_PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME = (
+ 'web_socket_passive_closing_handshake')
+
+
+class DispatchException(Exception):
+ """Exception in dispatching WebSocket request."""
+
+ def __init__(self, name, status=common.HTTP_STATUS_NOT_FOUND):
+ super(DispatchException, self).__init__(name)
+ self.status = status
+
+
+def _default_passive_closing_handshake_handler(request):
+ """Default web_socket_passive_closing_handshake handler."""
+
+ return common.STATUS_NORMAL_CLOSURE, ''
+
+
+def _normalize_path(path):
+ """Normalize path.
+
+ Args:
+ path: the path to normalize.
+
+ Path is converted to the absolute path.
+ The input path can use either '\\' or '/' as the separator.
+ The normalized path always uses '/' regardless of the platform.
+ """
+
+ path = path.replace('\\', os.path.sep)
+ path = os.path.realpath(path)
+ path = path.replace('\\', '/')
+ return path
+
+
+def _create_path_to_resource_converter(base_dir):
+ """Returns a function that converts the path of a WebSocket handler source
+ file to a resource string by removing the path to the base directory from
+ its head, removing _SOURCE_SUFFIX from its tail, and replacing path
+ separators in it with '/'.
+
+ Args:
+ base_dir: the path to the base directory.
+ """
+
+ base_dir = _normalize_path(base_dir)
+
+ base_len = len(base_dir)
+ suffix_len = len(_SOURCE_SUFFIX)
+
+ def converter(path):
+ if not path.endswith(_SOURCE_SUFFIX):
+ return None
+ # _normalize_path must not be used because resolving symlink breaks
+ # following path check.
+ path = path.replace('\\', '/')
+ if not path.startswith(base_dir):
+ return None
+ return path[base_len:-suffix_len]
+
+ return converter
+
+
+def _enumerate_handler_file_paths(directory):
+ """Returns a generator that enumerates WebSocket Handler source file names
+ in the given directory.
+ """
+
+ for root, unused_dirs, files in os.walk(directory):
+ for base in files:
+ path = os.path.join(root, base)
+ if _SOURCE_PATH_PATTERN.search(path):
+ yield path
+
+
+class _HandlerSuite(object):
+ """A handler suite holder class."""
+
+ def __init__(self, do_extra_handshake, transfer_data,
+ passive_closing_handshake):
+ self.do_extra_handshake = do_extra_handshake
+ self.transfer_data = transfer_data
+ self.passive_closing_handshake = passive_closing_handshake
+
+
+def _source_handler_file(handler_definition):
+ """Source a handler definition string.
+
+ Args:
+ handler_definition: a string containing Python statements that define
+ handler functions.
+ """
+
+ global_dic = {}
+ try:
+ exec handler_definition in global_dic
+ except Exception:
+ raise DispatchException('Error in sourcing handler:' +
+ util.get_stack_trace())
+ passive_closing_handshake_handler = None
+ try:
+ passive_closing_handshake_handler = _extract_handler(
+ global_dic, _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME)
+ except Exception:
+ passive_closing_handshake_handler = (
+ _default_passive_closing_handshake_handler)
+ return _HandlerSuite(
+ _extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME),
+ _extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME),
+ passive_closing_handshake_handler)
+
+
+def _extract_handler(dic, name):
+ """Extracts a callable with the specified name from the given dictionary
+ dic.
+ """
+
+ if name not in dic:
+ raise DispatchException('%s is not defined.' % name)
+ handler = dic[name]
+ if not callable(handler):
+ raise DispatchException('%s is not callable.' % name)
+ return handler
+
+
+class Dispatcher(object):
+ """Dispatches WebSocket requests.
+
+ This class maintains a map from resource name to handlers.
+ """
+
+ def __init__(
+ self, root_dir, scan_dir=None,
+ allow_handlers_outside_root_dir=True):
+ """Construct an instance.
+
+ Args:
+ root_dir: The directory where handler definition files are
+ placed.
+ scan_dir: The directory where handler definition files are
+ searched. scan_dir must be a directory under root_dir,
+ including root_dir itself. If scan_dir is None,
+ root_dir is used as scan_dir. scan_dir can be useful
+ in saving scan time when root_dir contains many
+ subdirectories.
+ allow_handlers_outside_root_dir: Scans handler files even if their
+ canonical path is not under root_dir.
+ """
+
+ self._logger = util.get_class_logger(self)
+
+ self._handler_suite_map = {}
+ self._source_warnings = []
+ if scan_dir is None:
+ scan_dir = root_dir
+ if not os.path.realpath(scan_dir).startswith(
+ os.path.realpath(root_dir)):
+ raise DispatchException('scan_dir:%s must be a directory under '
+ 'root_dir:%s.' % (scan_dir, root_dir))
+ self._source_handler_files_in_dir(
+ root_dir, scan_dir, allow_handlers_outside_root_dir)
+
+ def add_resource_path_alias(self,
+ alias_resource_path, existing_resource_path):
+ """Add resource path alias.
+
+ Once added, request to alias_resource_path would be handled by
+ handler registered for existing_resource_path.
+
+ Args:
+ alias_resource_path: alias resource path
+ existing_resource_path: existing resource path
+ """
+ try:
+ handler_suite = self._handler_suite_map[existing_resource_path]
+ self._handler_suite_map[alias_resource_path] = handler_suite
+ except KeyError:
+ raise DispatchException('No handler for: %r' %
+ existing_resource_path)
+
+ def source_warnings(self):
+ """Return warnings in sourcing handlers."""
+
+ return self._source_warnings
+
+ def do_extra_handshake(self, request):
+ """Do extra checking in WebSocket handshake.
+
+ Select a handler based on request.uri and call its
+ web_socket_do_extra_handshake function.
+
+ Args:
+ request: mod_python request.
+
+ Raises:
+ DispatchException: when handler was not found
+ AbortedByUserException: when user handler abort connection
+ HandshakeException: when opening handshake failed
+ """
+
+ handler_suite = self.get_handler_suite(request.ws_resource)
+ if handler_suite is None:
+ raise DispatchException('No handler for: %r' % request.ws_resource)
+ do_extra_handshake_ = handler_suite.do_extra_handshake
+ try:
+ do_extra_handshake_(request)
+ except handshake.AbortedByUserException, e:
+ # Re-raise to tell the caller of this function to finish this
+ # connection without sending any error.
+ self._logger.debug('%s', util.get_stack_trace())
+ raise
+ except Exception, e:
+ util.prepend_message_to_exception(
+ '%s raised exception for %s: ' % (
+ _DO_EXTRA_HANDSHAKE_HANDLER_NAME,
+ request.ws_resource),
+ e)
+ raise handshake.HandshakeException(e, common.HTTP_STATUS_FORBIDDEN)
+
+ def transfer_data(self, request):
+ """Let a handler transfer_data with a WebSocket client.
+
+ Select a handler based on request.ws_resource and call its
+ web_socket_transfer_data function.
+
+ Args:
+ request: mod_python request.
+
+ Raises:
+ DispatchException: when handler was not found
+ AbortedByUserException: when user handler abort connection
+ """
+
+ # TODO(tyoshino): Terminate underlying TCP connection if possible.
+ try:
+ if mux.use_mux(request):
+ mux.start(request, self)
+ else:
+ handler_suite = self.get_handler_suite(request.ws_resource)
+ if handler_suite is None:
+ raise DispatchException('No handler for: %r' %
+ request.ws_resource)
+ transfer_data_ = handler_suite.transfer_data
+ transfer_data_(request)
+
+ if not request.server_terminated:
+ request.ws_stream.close_connection()
+ # Catch non-critical exceptions the handler didn't handle.
+ except handshake.AbortedByUserException, e:
+ self._logger.debug('%s', util.get_stack_trace())
+ raise
+ except msgutil.BadOperationException, e:
+ self._logger.debug('%s', e)
+ request.ws_stream.close_connection(
+ common.STATUS_INTERNAL_ENDPOINT_ERROR)
+ except msgutil.InvalidFrameException, e:
+ # InvalidFrameException must be caught before
+ # ConnectionTerminatedException that catches InvalidFrameException.
+ self._logger.debug('%s', e)
+ request.ws_stream.close_connection(common.STATUS_PROTOCOL_ERROR)
+ except msgutil.UnsupportedFrameException, e:
+ self._logger.debug('%s', e)
+ request.ws_stream.close_connection(common.STATUS_UNSUPPORTED_DATA)
+ except stream.InvalidUTF8Exception, e:
+ self._logger.debug('%s', e)
+ request.ws_stream.close_connection(
+ common.STATUS_INVALID_FRAME_PAYLOAD_DATA)
+ except msgutil.ConnectionTerminatedException, e:
+ self._logger.debug('%s', e)
+ except Exception, e:
+ # Any other exceptions are forwarded to the caller of this
+ # function.
+ util.prepend_message_to_exception(
+ '%s raised exception for %s: ' % (
+ _TRANSFER_DATA_HANDLER_NAME, request.ws_resource),
+ e)
+ raise
+
+ def passive_closing_handshake(self, request):
+ """Prepare code and reason for responding client initiated closing
+ handshake.
+ """
+
+ handler_suite = self.get_handler_suite(request.ws_resource)
+ if handler_suite is None:
+ return _default_passive_closing_handshake_handler(request)
+ return handler_suite.passive_closing_handshake(request)
+
+ def get_handler_suite(self, resource):
+ """Retrieves two handlers (one for extra handshake processing, and one
+ for data transfer) for the given request as a HandlerSuite object.
+ """
+
+ fragment = None
+ if '#' in resource:
+ resource, fragment = resource.split('#', 1)
+ if '?' in resource:
+ resource = resource.split('?', 1)[0]
+ handler_suite = self._handler_suite_map.get(resource)
+ if handler_suite and fragment:
+ raise DispatchException('Fragment identifiers MUST NOT be used on '
+ 'WebSocket URIs',
+ common.HTTP_STATUS_BAD_REQUEST)
+ return handler_suite
+
+ def _source_handler_files_in_dir(
+ self, root_dir, scan_dir, allow_handlers_outside_root_dir):
+ """Source all the handler source files in the scan_dir directory.
+
+ The resource path is determined relative to root_dir.
+ """
+
+ # We build a map from resource to handler code assuming that there's
+ # only one path from root_dir to scan_dir and it can be obtained by
+ # comparing realpath of them.
+
+ # Here we cannot use abspath. See
+ # https://bugs.webkit.org/show_bug.cgi?id=31603
+
+ convert = _create_path_to_resource_converter(root_dir)
+ scan_realpath = os.path.realpath(scan_dir)
+ root_realpath = os.path.realpath(root_dir)
+ for path in _enumerate_handler_file_paths(scan_realpath):
+ if (not allow_handlers_outside_root_dir and
+ (not os.path.realpath(path).startswith(root_realpath))):
+ self._logger.debug(
+ 'Canonical path of %s is not under root directory' %
+ path)
+ continue
+ try:
+ handler_suite = _source_handler_file(open(path).read())
+ except DispatchException, e:
+ self._source_warnings.append('%s: %s' % (path, e))
+ continue
+ resource = convert(path)
+ if resource is None:
+ self._logger.debug(
+ 'Path to resource conversion on %s failed' % path)
+ else:
+ self._handler_suite_map[convert(path)] = handler_suite
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/extensions.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/extensions.py
new file mode 100644
index 000000000..49a9fdcf9
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/extensions.py
@@ -0,0 +1,885 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+from mod_pywebsocket import common
+from mod_pywebsocket import util
+from mod_pywebsocket.http_header_util import quote_if_necessary
+
+
+# The list of available server side extension processor classes.
+_available_processors = {}
+_compression_extension_names = []
+
+
+class ExtensionProcessorInterface(object):
+
+ def __init__(self, request):
+ self._logger = util.get_class_logger(self)
+
+ self._request = request
+ self._active = True
+
+ def request(self):
+ return self._request
+
+ def name(self):
+ return None
+
+ def check_consistency_with_other_processors(self, processors):
+ pass
+
+ def set_active(self, active):
+ self._active = active
+
+ def is_active(self):
+ return self._active
+
+ def _get_extension_response_internal(self):
+ return None
+
+ def get_extension_response(self):
+ if not self._active:
+ self._logger.debug('Extension %s is deactivated', self.name())
+ return None
+
+ response = self._get_extension_response_internal()
+ if response is None:
+ self._active = False
+ return response
+
+ def _setup_stream_options_internal(self, stream_options):
+ pass
+
+ def setup_stream_options(self, stream_options):
+ if self._active:
+ self._setup_stream_options_internal(stream_options)
+
+
+def _log_outgoing_compression_ratio(
+ logger, original_bytes, filtered_bytes, average_ratio):
+ # Print inf when ratio is not available.
+ ratio = float('inf')
+ if original_bytes != 0:
+ ratio = float(filtered_bytes) / original_bytes
+
+ logger.debug('Outgoing compression ratio: %f (average: %f)' %
+ (ratio, average_ratio))
+
+
+def _log_incoming_compression_ratio(
+ logger, received_bytes, filtered_bytes, average_ratio):
+ # Print inf when ratio is not available.
+ ratio = float('inf')
+ if filtered_bytes != 0:
+ ratio = float(received_bytes) / filtered_bytes
+
+ logger.debug('Incoming compression ratio: %f (average: %f)' %
+ (ratio, average_ratio))
+
+
+def _parse_window_bits(bits):
+ """Return parsed integer value iff the given string conforms to the
+ grammar of the window bits extension parameters.
+ """
+
+ if bits is None:
+ raise ValueError('Value is required')
+
+ # For non integer values such as "10.0", ValueError will be raised.
+ int_bits = int(bits)
+
+ # First condition is to drop leading zero case e.g. "08".
+ if bits != str(int_bits) or int_bits < 8 or int_bits > 15:
+ raise ValueError('Invalid value: %r' % bits)
+
+ return int_bits
+
+
+class _AverageRatioCalculator(object):
+ """Stores total bytes of original and result data, and calculates average
+ result / original ratio.
+ """
+
+ def __init__(self):
+ self._total_original_bytes = 0
+ self._total_result_bytes = 0
+
+ def add_original_bytes(self, value):
+ self._total_original_bytes += value
+
+ def add_result_bytes(self, value):
+ self._total_result_bytes += value
+
+ def get_average_ratio(self):
+ if self._total_original_bytes != 0:
+ return (float(self._total_result_bytes) /
+ self._total_original_bytes)
+ else:
+ return float('inf')
+
+
+class DeflateFrameExtensionProcessor(ExtensionProcessorInterface):
+ """deflate-frame extension processor.
+
+ Specification:
+ http://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate
+ """
+
+ _WINDOW_BITS_PARAM = 'max_window_bits'
+ _NO_CONTEXT_TAKEOVER_PARAM = 'no_context_takeover'
+
+ def __init__(self, request):
+ ExtensionProcessorInterface.__init__(self, request)
+ self._logger = util.get_class_logger(self)
+
+ self._response_window_bits = None
+ self._response_no_context_takeover = False
+ self._bfinal = False
+
+ # Calculates
+ # (Total outgoing bytes supplied to this filter) /
+ # (Total bytes sent to the network after applying this filter)
+ self._outgoing_average_ratio_calculator = _AverageRatioCalculator()
+
+ # Calculates
+ # (Total bytes received from the network) /
+ # (Total incoming bytes obtained after applying this filter)
+ self._incoming_average_ratio_calculator = _AverageRatioCalculator()
+
+ def name(self):
+ return common.DEFLATE_FRAME_EXTENSION
+
+ def _get_extension_response_internal(self):
+ # Any unknown parameter will be just ignored.
+
+ window_bits = None
+ if self._request.has_parameter(self._WINDOW_BITS_PARAM):
+ window_bits = self._request.get_parameter_value(
+ self._WINDOW_BITS_PARAM)
+ try:
+ window_bits = _parse_window_bits(window_bits)
+ except ValueError, e:
+ return None
+
+ no_context_takeover = self._request.has_parameter(
+ self._NO_CONTEXT_TAKEOVER_PARAM)
+ if (no_context_takeover and
+ self._request.get_parameter_value(
+ self._NO_CONTEXT_TAKEOVER_PARAM) is not None):
+ return None
+
+ self._rfc1979_deflater = util._RFC1979Deflater(
+ window_bits, no_context_takeover)
+
+ self._rfc1979_inflater = util._RFC1979Inflater()
+
+ self._compress_outgoing = True
+
+ response = common.ExtensionParameter(self._request.name())
+
+ if self._response_window_bits is not None:
+ response.add_parameter(
+ self._WINDOW_BITS_PARAM, str(self._response_window_bits))
+ if self._response_no_context_takeover:
+ response.add_parameter(
+ self._NO_CONTEXT_TAKEOVER_PARAM, None)
+
+ self._logger.debug(
+ 'Enable %s extension ('
+ 'request: window_bits=%s; no_context_takeover=%r, '
+ 'response: window_wbits=%s; no_context_takeover=%r)' %
+ (self._request.name(),
+ window_bits,
+ no_context_takeover,
+ self._response_window_bits,
+ self._response_no_context_takeover))
+
+ return response
+
+ def _setup_stream_options_internal(self, stream_options):
+
+ class _OutgoingFilter(object):
+
+ def __init__(self, parent):
+ self._parent = parent
+
+ def filter(self, frame):
+ self._parent._outgoing_filter(frame)
+
+ class _IncomingFilter(object):
+
+ def __init__(self, parent):
+ self._parent = parent
+
+ def filter(self, frame):
+ self._parent._incoming_filter(frame)
+
+ stream_options.outgoing_frame_filters.append(
+ _OutgoingFilter(self))
+ stream_options.incoming_frame_filters.insert(
+ 0, _IncomingFilter(self))
+
+ def set_response_window_bits(self, value):
+ self._response_window_bits = value
+
+ def set_response_no_context_takeover(self, value):
+ self._response_no_context_takeover = value
+
+ def set_bfinal(self, value):
+ self._bfinal = value
+
+ def enable_outgoing_compression(self):
+ self._compress_outgoing = True
+
+ def disable_outgoing_compression(self):
+ self._compress_outgoing = False
+
+ def _outgoing_filter(self, frame):
+ """Transform outgoing frames. This method is called only by
+ an _OutgoingFilter instance.
+ """
+
+ original_payload_size = len(frame.payload)
+ self._outgoing_average_ratio_calculator.add_original_bytes(
+ original_payload_size)
+
+ if (not self._compress_outgoing or
+ common.is_control_opcode(frame.opcode)):
+ self._outgoing_average_ratio_calculator.add_result_bytes(
+ original_payload_size)
+ return
+
+ frame.payload = self._rfc1979_deflater.filter(
+ frame.payload, bfinal=self._bfinal)
+ frame.rsv1 = 1
+
+ filtered_payload_size = len(frame.payload)
+ self._outgoing_average_ratio_calculator.add_result_bytes(
+ filtered_payload_size)
+
+ _log_outgoing_compression_ratio(
+ self._logger,
+ original_payload_size,
+ filtered_payload_size,
+ self._outgoing_average_ratio_calculator.get_average_ratio())
+
+ def _incoming_filter(self, frame):
+ """Transform incoming frames. This method is called only by
+ an _IncomingFilter instance.
+ """
+
+ received_payload_size = len(frame.payload)
+ self._incoming_average_ratio_calculator.add_result_bytes(
+ received_payload_size)
+
+ if frame.rsv1 != 1 or common.is_control_opcode(frame.opcode):
+ self._incoming_average_ratio_calculator.add_original_bytes(
+ received_payload_size)
+ return
+
+ frame.payload = self._rfc1979_inflater.filter(frame.payload)
+ frame.rsv1 = 0
+
+ filtered_payload_size = len(frame.payload)
+ self._incoming_average_ratio_calculator.add_original_bytes(
+ filtered_payload_size)
+
+ _log_incoming_compression_ratio(
+ self._logger,
+ received_payload_size,
+ filtered_payload_size,
+ self._incoming_average_ratio_calculator.get_average_ratio())
+
+
+_available_processors[common.DEFLATE_FRAME_EXTENSION] = (
+ DeflateFrameExtensionProcessor)
+_compression_extension_names.append(common.DEFLATE_FRAME_EXTENSION)
+
+_available_processors[common.X_WEBKIT_DEFLATE_FRAME_EXTENSION] = (
+ DeflateFrameExtensionProcessor)
+_compression_extension_names.append(common.X_WEBKIT_DEFLATE_FRAME_EXTENSION)
+
+
+def _parse_compression_method(data):
+ """Parses the value of "method" extension parameter."""
+
+ return common.parse_extensions(data)
+
+
+def _create_accepted_method_desc(method_name, method_params):
+ """Creates accepted-method-desc from given method name and parameters"""
+
+ extension = common.ExtensionParameter(method_name)
+ for name, value in method_params:
+ extension.add_parameter(name, value)
+ return common.format_extension(extension)
+
+
+class CompressionExtensionProcessorBase(ExtensionProcessorInterface):
+ """Base class for perframe-compress and permessage-compress extension."""
+
+ _METHOD_PARAM = 'method'
+
+ def __init__(self, request):
+ ExtensionProcessorInterface.__init__(self, request)
+ self._logger = util.get_class_logger(self)
+ self._compression_method_name = None
+ self._compression_processor = None
+ self._compression_processor_hook = None
+
+ def name(self):
+ return ''
+
+ def _lookup_compression_processor(self, method_desc):
+ return None
+
+ def _get_compression_processor_response(self):
+ """Looks up the compression processor based on the self._request and
+ returns the compression processor's response.
+ """
+
+ method_list = self._request.get_parameter_value(self._METHOD_PARAM)
+ if method_list is None:
+ return None
+ methods = _parse_compression_method(method_list)
+ if methods is None:
+ return None
+ comression_processor = None
+ # The current implementation tries only the first method that matches
+ # supported algorithm. Following methods aren't tried even if the
+ # first one is rejected.
+ # TODO(bashi): Need to clarify this behavior.
+ for method_desc in methods:
+ compression_processor = self._lookup_compression_processor(
+ method_desc)
+ if compression_processor is not None:
+ self._compression_method_name = method_desc.name()
+ break
+ if compression_processor is None:
+ return None
+
+ if self._compression_processor_hook:
+ self._compression_processor_hook(compression_processor)
+
+ processor_response = compression_processor.get_extension_response()
+ if processor_response is None:
+ return None
+ self._compression_processor = compression_processor
+ return processor_response
+
+ def _get_extension_response_internal(self):
+ processor_response = self._get_compression_processor_response()
+ if processor_response is None:
+ return None
+
+ response = common.ExtensionParameter(self._request.name())
+ accepted_method_desc = _create_accepted_method_desc(
+ self._compression_method_name,
+ processor_response.get_parameters())
+ response.add_parameter(self._METHOD_PARAM, accepted_method_desc)
+ self._logger.debug(
+ 'Enable %s extension (method: %s)' %
+ (self._request.name(), self._compression_method_name))
+ return response
+
+ def _setup_stream_options_internal(self, stream_options):
+ if self._compression_processor is None:
+ return
+ self._compression_processor.setup_stream_options(stream_options)
+
+ def set_compression_processor_hook(self, hook):
+ self._compression_processor_hook = hook
+
+ def get_compression_processor(self):
+ return self._compression_processor
+
+
+class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface):
+ """permessage-deflate extension processor. It's also used for
+ permessage-compress extension when the deflate method is chosen.
+
+ Specification:
+ http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-08
+ """
+
+ _SERVER_MAX_WINDOW_BITS_PARAM = 'server_max_window_bits'
+ _SERVER_NO_CONTEXT_TAKEOVER_PARAM = 'server_no_context_takeover'
+ _CLIENT_MAX_WINDOW_BITS_PARAM = 'client_max_window_bits'
+ _CLIENT_NO_CONTEXT_TAKEOVER_PARAM = 'client_no_context_takeover'
+
+ def __init__(self, request, draft08=True):
+ """Construct PerMessageDeflateExtensionProcessor
+
+ Args:
+ draft08: Follow the constraints on the parameters that were not
+ specified for permessage-compress but are specified for
+ permessage-deflate as on
+ draft-ietf-hybi-permessage-compression-08.
+ """
+
+ ExtensionProcessorInterface.__init__(self, request)
+ self._logger = util.get_class_logger(self)
+
+ self._preferred_client_max_window_bits = None
+ self._client_no_context_takeover = False
+
+ self._draft08 = draft08
+
+ def name(self):
+ return 'deflate'
+
+ def _get_extension_response_internal(self):
+ if self._draft08:
+ for name in self._request.get_parameter_names():
+ if name not in [self._SERVER_MAX_WINDOW_BITS_PARAM,
+ self._SERVER_NO_CONTEXT_TAKEOVER_PARAM,
+ self._CLIENT_MAX_WINDOW_BITS_PARAM]:
+ self._logger.debug('Unknown parameter: %r', name)
+ return None
+ else:
+ # Any unknown parameter will be just ignored.
+ pass
+
+ server_max_window_bits = None
+ if self._request.has_parameter(self._SERVER_MAX_WINDOW_BITS_PARAM):
+ server_max_window_bits = self._request.get_parameter_value(
+ self._SERVER_MAX_WINDOW_BITS_PARAM)
+ try:
+ server_max_window_bits = _parse_window_bits(
+ server_max_window_bits)
+ except ValueError, e:
+ self._logger.debug('Bad %s parameter: %r',
+ self._SERVER_MAX_WINDOW_BITS_PARAM,
+ e)
+ return None
+
+ server_no_context_takeover = self._request.has_parameter(
+ self._SERVER_NO_CONTEXT_TAKEOVER_PARAM)
+ if (server_no_context_takeover and
+ self._request.get_parameter_value(
+ self._SERVER_NO_CONTEXT_TAKEOVER_PARAM) is not None):
+ self._logger.debug('%s parameter must not have a value: %r',
+ self._SERVER_NO_CONTEXT_TAKEOVER_PARAM,
+ server_no_context_takeover)
+ return None
+
+ # client_max_window_bits from a client indicates whether the client can
+ # accept client_max_window_bits from a server or not.
+ client_client_max_window_bits = self._request.has_parameter(
+ self._CLIENT_MAX_WINDOW_BITS_PARAM)
+ if (self._draft08 and
+ client_client_max_window_bits and
+ self._request.get_parameter_value(
+ self._CLIENT_MAX_WINDOW_BITS_PARAM) is not None):
+ self._logger.debug('%s parameter must not have a value in a '
+ 'client\'s opening handshake: %r',
+ self._CLIENT_MAX_WINDOW_BITS_PARAM,
+ client_client_max_window_bits)
+ return None
+
+ self._rfc1979_deflater = util._RFC1979Deflater(
+ server_max_window_bits, server_no_context_takeover)
+
+ # Note that we prepare for incoming messages compressed with window
+ # bits upto 15 regardless of the client_max_window_bits value to be
+ # sent to the client.
+ self._rfc1979_inflater = util._RFC1979Inflater()
+
+ self._framer = _PerMessageDeflateFramer(
+ server_max_window_bits, server_no_context_takeover)
+ self._framer.set_bfinal(False)
+ self._framer.set_compress_outgoing_enabled(True)
+
+ response = common.ExtensionParameter(self._request.name())
+
+ if server_max_window_bits is not None:
+ response.add_parameter(
+ self._SERVER_MAX_WINDOW_BITS_PARAM,
+ str(server_max_window_bits))
+
+ if server_no_context_takeover:
+ response.add_parameter(
+ self._SERVER_NO_CONTEXT_TAKEOVER_PARAM, None)
+
+ if self._preferred_client_max_window_bits is not None:
+ if self._draft08 and not client_client_max_window_bits:
+ self._logger.debug('Processor is configured to use %s but '
+ 'the client cannot accept it',
+ self._CLIENT_MAX_WINDOW_BITS_PARAM)
+ return None
+ response.add_parameter(
+ self._CLIENT_MAX_WINDOW_BITS_PARAM,
+ str(self._preferred_client_max_window_bits))
+
+ if self._client_no_context_takeover:
+ response.add_parameter(
+ self._CLIENT_NO_CONTEXT_TAKEOVER_PARAM, None)
+
+ self._logger.debug(
+ 'Enable %s extension ('
+ 'request: server_max_window_bits=%s; '
+ 'server_no_context_takeover=%r, '
+ 'response: client_max_window_bits=%s; '
+ 'client_no_context_takeover=%r)' %
+ (self._request.name(),
+ server_max_window_bits,
+ server_no_context_takeover,
+ self._preferred_client_max_window_bits,
+ self._client_no_context_takeover))
+
+ return response
+
+ def _setup_stream_options_internal(self, stream_options):
+ self._framer.setup_stream_options(stream_options)
+
+ def set_client_max_window_bits(self, value):
+ """If this option is specified, this class adds the
+ client_max_window_bits extension parameter to the handshake response,
+ but doesn't reduce the LZ77 sliding window size of its inflater.
+ I.e., you can use this for testing client implementation but cannot
+ reduce memory usage of this class.
+
+ If this method has been called with True and an offer without the
+ client_max_window_bits extension parameter is received,
+ - (When processing the permessage-deflate extension) this processor
+ declines the request.
+ - (When processing the permessage-compress extension) this processor
+ accepts the request.
+ """
+
+ self._preferred_client_max_window_bits = value
+
+ def set_client_no_context_takeover(self, value):
+ """If this option is specified, this class adds the
+ client_no_context_takeover extension parameter to the handshake
+ response, but doesn't reset inflater for each message. I.e., you can
+ use this for testing client implementation but cannot reduce memory
+ usage of this class.
+ """
+
+ self._client_no_context_takeover = value
+
+ def set_bfinal(self, value):
+ self._framer.set_bfinal(value)
+
+ def enable_outgoing_compression(self):
+ self._framer.set_compress_outgoing_enabled(True)
+
+ def disable_outgoing_compression(self):
+ self._framer.set_compress_outgoing_enabled(False)
+
+
+class _PerMessageDeflateFramer(object):
+ """A framer for extensions with per-message DEFLATE feature."""
+
+ def __init__(self, deflate_max_window_bits, deflate_no_context_takeover):
+ self._logger = util.get_class_logger(self)
+
+ self._rfc1979_deflater = util._RFC1979Deflater(
+ deflate_max_window_bits, deflate_no_context_takeover)
+
+ self._rfc1979_inflater = util._RFC1979Inflater()
+
+ self._bfinal = False
+
+ self._compress_outgoing_enabled = False
+
+ # True if a message is fragmented and compression is ongoing.
+ self._compress_ongoing = False
+
+ # Calculates
+ # (Total outgoing bytes supplied to this filter) /
+ # (Total bytes sent to the network after applying this filter)
+ self._outgoing_average_ratio_calculator = _AverageRatioCalculator()
+
+ # Calculates
+ # (Total bytes received from the network) /
+ # (Total incoming bytes obtained after applying this filter)
+ self._incoming_average_ratio_calculator = _AverageRatioCalculator()
+
+ def set_bfinal(self, value):
+ self._bfinal = value
+
+ def set_compress_outgoing_enabled(self, value):
+ self._compress_outgoing_enabled = value
+
+ def _process_incoming_message(self, message, decompress):
+ if not decompress:
+ return message
+
+ received_payload_size = len(message)
+ self._incoming_average_ratio_calculator.add_result_bytes(
+ received_payload_size)
+
+ message = self._rfc1979_inflater.filter(message)
+
+ filtered_payload_size = len(message)
+ self._incoming_average_ratio_calculator.add_original_bytes(
+ filtered_payload_size)
+
+ _log_incoming_compression_ratio(
+ self._logger,
+ received_payload_size,
+ filtered_payload_size,
+ self._incoming_average_ratio_calculator.get_average_ratio())
+
+ return message
+
+ def _process_outgoing_message(self, message, end, binary):
+ if not binary:
+ message = message.encode('utf-8')
+
+ if not self._compress_outgoing_enabled:
+ return message
+
+ original_payload_size = len(message)
+ self._outgoing_average_ratio_calculator.add_original_bytes(
+ original_payload_size)
+
+ message = self._rfc1979_deflater.filter(
+ message, end=end, bfinal=self._bfinal)
+
+ filtered_payload_size = len(message)
+ self._outgoing_average_ratio_calculator.add_result_bytes(
+ filtered_payload_size)
+
+ _log_outgoing_compression_ratio(
+ self._logger,
+ original_payload_size,
+ filtered_payload_size,
+ self._outgoing_average_ratio_calculator.get_average_ratio())
+
+ if not self._compress_ongoing:
+ self._outgoing_frame_filter.set_compression_bit()
+ self._compress_ongoing = not end
+ return message
+
+ def _process_incoming_frame(self, frame):
+ if frame.rsv1 == 1 and not common.is_control_opcode(frame.opcode):
+ self._incoming_message_filter.decompress_next_message()
+ frame.rsv1 = 0
+
+ def _process_outgoing_frame(self, frame, compression_bit):
+ if (not compression_bit or
+ common.is_control_opcode(frame.opcode)):
+ return
+
+ frame.rsv1 = 1
+
+ def setup_stream_options(self, stream_options):
+ """Creates filters and sets them to the StreamOptions."""
+
+ class _OutgoingMessageFilter(object):
+
+ def __init__(self, parent):
+ self._parent = parent
+
+ def filter(self, message, end=True, binary=False):
+ return self._parent._process_outgoing_message(
+ message, end, binary)
+
+ class _IncomingMessageFilter(object):
+
+ def __init__(self, parent):
+ self._parent = parent
+ self._decompress_next_message = False
+
+ def decompress_next_message(self):
+ self._decompress_next_message = True
+
+ def filter(self, message):
+ message = self._parent._process_incoming_message(
+ message, self._decompress_next_message)
+ self._decompress_next_message = False
+ return message
+
+ self._outgoing_message_filter = _OutgoingMessageFilter(self)
+ self._incoming_message_filter = _IncomingMessageFilter(self)
+ stream_options.outgoing_message_filters.append(
+ self._outgoing_message_filter)
+ stream_options.incoming_message_filters.append(
+ self._incoming_message_filter)
+
+ class _OutgoingFrameFilter(object):
+
+ def __init__(self, parent):
+ self._parent = parent
+ self._set_compression_bit = False
+
+ def set_compression_bit(self):
+ self._set_compression_bit = True
+
+ def filter(self, frame):
+ self._parent._process_outgoing_frame(
+ frame, self._set_compression_bit)
+ self._set_compression_bit = False
+
+ class _IncomingFrameFilter(object):
+
+ def __init__(self, parent):
+ self._parent = parent
+
+ def filter(self, frame):
+ self._parent._process_incoming_frame(frame)
+
+ self._outgoing_frame_filter = _OutgoingFrameFilter(self)
+ self._incoming_frame_filter = _IncomingFrameFilter(self)
+ stream_options.outgoing_frame_filters.append(
+ self._outgoing_frame_filter)
+ stream_options.incoming_frame_filters.append(
+ self._incoming_frame_filter)
+
+ stream_options.encode_text_message_to_utf8 = False
+
+
+_available_processors[common.PERMESSAGE_DEFLATE_EXTENSION] = (
+ PerMessageDeflateExtensionProcessor)
+# TODO(tyoshino): Reorganize class names.
+_compression_extension_names.append('deflate')
+
+
+class PerMessageCompressExtensionProcessor(
+ CompressionExtensionProcessorBase):
+ """permessage-compress extension processor.
+
+ Specification:
+ http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression
+ """
+
+ _DEFLATE_METHOD = 'deflate'
+
+ def __init__(self, request):
+ CompressionExtensionProcessorBase.__init__(self, request)
+
+ def name(self):
+ return common.PERMESSAGE_COMPRESSION_EXTENSION
+
+ def _lookup_compression_processor(self, method_desc):
+ if method_desc.name() == self._DEFLATE_METHOD:
+ return PerMessageDeflateExtensionProcessor(method_desc, False)
+ return None
+
+
+_available_processors[common.PERMESSAGE_COMPRESSION_EXTENSION] = (
+ PerMessageCompressExtensionProcessor)
+_compression_extension_names.append(common.PERMESSAGE_COMPRESSION_EXTENSION)
+
+
+class MuxExtensionProcessor(ExtensionProcessorInterface):
+ """WebSocket multiplexing extension processor."""
+
+ _QUOTA_PARAM = 'quota'
+
+ def __init__(self, request):
+ ExtensionProcessorInterface.__init__(self, request)
+ self._quota = 0
+ self._extensions = []
+
+ def name(self):
+ return common.MUX_EXTENSION
+
+ def check_consistency_with_other_processors(self, processors):
+ before_mux = True
+ for processor in processors:
+ name = processor.name()
+ if name == self.name():
+ before_mux = False
+ continue
+ if not processor.is_active():
+ continue
+ if before_mux:
+ # Mux extension cannot be used after extensions
+ # that depend on frame boundary, extension data field, or any
+ # reserved bits which are attributed to each frame.
+ if (name == common.DEFLATE_FRAME_EXTENSION or
+ name == common.X_WEBKIT_DEFLATE_FRAME_EXTENSION):
+ self.set_active(False)
+ return
+ else:
+ # Mux extension should not be applied before any history-based
+ # compression extension.
+ if (name == common.DEFLATE_FRAME_EXTENSION or
+ name == common.X_WEBKIT_DEFLATE_FRAME_EXTENSION or
+ name == common.PERMESSAGE_COMPRESSION_EXTENSION or
+ name == common.X_WEBKIT_PERMESSAGE_COMPRESSION_EXTENSION):
+ self.set_active(False)
+ return
+
+ def _get_extension_response_internal(self):
+ self._active = False
+ quota = self._request.get_parameter_value(self._QUOTA_PARAM)
+ if quota is not None:
+ try:
+ quota = int(quota)
+ except ValueError, e:
+ return None
+ if quota < 0 or quota >= 2 ** 32:
+ return None
+ self._quota = quota
+
+ self._active = True
+ return common.ExtensionParameter(common.MUX_EXTENSION)
+
+ def _setup_stream_options_internal(self, stream_options):
+ pass
+
+ def set_quota(self, quota):
+ self._quota = quota
+
+ def quota(self):
+ return self._quota
+
+ def set_extensions(self, extensions):
+ self._extensions = extensions
+
+ def extensions(self):
+ return self._extensions
+
+
+_available_processors[common.MUX_EXTENSION] = MuxExtensionProcessor
+
+
+def get_extension_processor(extension_request):
+ """Given an ExtensionParameter representing an extension offer received
+ from a client, configures and returns an instance of the corresponding
+ extension processor class.
+ """
+
+ processor_class = _available_processors.get(extension_request.name())
+ if processor_class is None:
+ return None
+ return processor_class(extension_request)
+
+
+def is_compression_extension(extension_name):
+ return extension_name in _compression_extension_names
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/fast_masking.i b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/fast_masking.i
new file mode 100644
index 000000000..ddaad27f5
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/fast_masking.i
@@ -0,0 +1,98 @@
+// Copyright 2013, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+%module fast_masking
+
+%include "cstring.i"
+
+%{
+#include <cstring>
+
+#ifdef __SSE2__
+#include <emmintrin.h>
+#endif
+%}
+
+%apply (char *STRING, int LENGTH) {
+ (const char* payload, int payload_length),
+ (const char* masking_key, int masking_key_length) };
+%cstring_output_allocate_size(
+ char** result, int* result_length, delete [] *$1);
+
+%inline %{
+
+void mask(
+ const char* payload, int payload_length,
+ const char* masking_key, int masking_key_length,
+ int masking_key_index,
+ char** result, int* result_length) {
+ *result = new char[payload_length];
+ *result_length = payload_length;
+ memcpy(*result, payload, payload_length);
+
+ char* cursor = *result;
+ char* cursor_end = *result + *result_length;
+
+#ifdef __SSE2__
+ while ((cursor < cursor_end) &&
+ (reinterpret_cast<size_t>(cursor) & 0xf)) {
+ *cursor ^= masking_key[masking_key_index];
+ ++cursor;
+ masking_key_index = (masking_key_index + 1) % masking_key_length;
+ }
+ if (cursor == cursor_end) {
+ return;
+ }
+
+ const int kBlockSize = 16;
+ __m128i masking_key_block;
+ for (int i = 0; i < kBlockSize; ++i) {
+ *(reinterpret_cast<char*>(&masking_key_block) + i) =
+ masking_key[masking_key_index];
+ masking_key_index = (masking_key_index + 1) % masking_key_length;
+ }
+
+ while (cursor + kBlockSize <= cursor_end) {
+ __m128i payload_block =
+ _mm_load_si128(reinterpret_cast<__m128i*>(cursor));
+ _mm_stream_si128(reinterpret_cast<__m128i*>(cursor),
+ _mm_xor_si128(payload_block, masking_key_block));
+ cursor += kBlockSize;
+ }
+#endif
+
+ while (cursor < cursor_end) {
+ *cursor ^= masking_key[masking_key_index];
+ ++cursor;
+ masking_key_index = (masking_key_index + 1) % masking_key_length;
+ }
+}
+
+%}
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/__init__.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/__init__.py
new file mode 100644
index 000000000..194f6b395
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/__init__.py
@@ -0,0 +1,110 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""WebSocket opening handshake processor. This class try to apply available
+opening handshake processors for each protocol version until a connection is
+successfully established.
+"""
+
+
+import logging
+
+from mod_pywebsocket import common
+from mod_pywebsocket.handshake import hybi00
+from mod_pywebsocket.handshake import hybi
+# Export AbortedByUserException, HandshakeException, and VersionException
+# symbol from this module.
+from mod_pywebsocket.handshake._base import AbortedByUserException
+from mod_pywebsocket.handshake._base import HandshakeException
+from mod_pywebsocket.handshake._base import VersionException
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def do_handshake(request, dispatcher, allowDraft75=False, strict=False):
+ """Performs WebSocket handshake.
+
+ Args:
+ request: mod_python request.
+ dispatcher: Dispatcher (dispatch.Dispatcher).
+ allowDraft75: obsolete argument. ignored.
+ strict: obsolete argument. ignored.
+
+ Handshaker will add attributes such as ws_resource in performing
+ handshake.
+ """
+
+ _LOGGER.debug('Client\'s opening handshake resource: %r', request.uri)
+ # To print mimetools.Message as escaped one-line string, we converts
+ # headers_in to dict object. Without conversion, if we use %r, it just
+ # prints the type and address, and if we use %s, it prints the original
+ # header string as multiple lines.
+ #
+ # Both mimetools.Message and MpTable_Type of mod_python can be
+ # converted to dict.
+ #
+ # mimetools.Message.__str__ returns the original header string.
+ # dict(mimetools.Message object) returns the map from header names to
+ # header values. While MpTable_Type doesn't have such __str__ but just
+ # __repr__ which formats itself as well as dictionary object.
+ _LOGGER.debug(
+ 'Client\'s opening handshake headers: %r', dict(request.headers_in))
+
+ handshakers = []
+ handshakers.append(
+ ('RFC 6455', hybi.Handshaker(request, dispatcher)))
+ handshakers.append(
+ ('HyBi 00', hybi00.Handshaker(request, dispatcher)))
+
+ for name, handshaker in handshakers:
+ _LOGGER.debug('Trying protocol version %s', name)
+ try:
+ handshaker.do_handshake()
+ _LOGGER.info('Established (%s protocol)', name)
+ return
+ except HandshakeException, e:
+ _LOGGER.debug(
+ 'Failed to complete opening handshake as %s protocol: %r',
+ name, e)
+ if e.status:
+ raise e
+ except AbortedByUserException, e:
+ raise
+ except VersionException, e:
+ raise
+
+ # TODO(toyoshim): Add a test to cover the case all handshakers fail.
+ raise HandshakeException(
+ 'Failed to complete opening handshake for all available protocols',
+ status=common.HTTP_STATUS_BAD_REQUEST)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/_base.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/_base.py
new file mode 100644
index 000000000..c993a584b
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/_base.py
@@ -0,0 +1,182 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Common functions and exceptions used by WebSocket opening handshake
+processors.
+"""
+
+
+from mod_pywebsocket import common
+from mod_pywebsocket import http_header_util
+
+
+class AbortedByUserException(Exception):
+ """Exception for aborting a connection intentionally.
+
+ If this exception is raised in do_extra_handshake handler, the connection
+ will be abandoned. No other WebSocket or HTTP(S) handler will be invoked.
+
+ If this exception is raised in transfer_data_handler, the connection will
+ be closed without closing handshake. No other WebSocket or HTTP(S) handler
+ will be invoked.
+ """
+
+ pass
+
+
+class HandshakeException(Exception):
+ """This exception will be raised when an error occurred while processing
+ WebSocket initial handshake.
+ """
+
+ def __init__(self, name, status=None):
+ super(HandshakeException, self).__init__(name)
+ self.status = status
+
+
+class VersionException(Exception):
+ """This exception will be raised when a version of client request does not
+ match with version the server supports.
+ """
+
+ def __init__(self, name, supported_versions=''):
+ """Construct an instance.
+
+ Args:
+ supported_version: a str object to show supported hybi versions.
+ (e.g. '8, 13')
+ """
+ super(VersionException, self).__init__(name)
+ self.supported_versions = supported_versions
+
+
+def get_default_port(is_secure):
+ if is_secure:
+ return common.DEFAULT_WEB_SOCKET_SECURE_PORT
+ else:
+ return common.DEFAULT_WEB_SOCKET_PORT
+
+
+def validate_subprotocol(subprotocol):
+ """Validate a value in the Sec-WebSocket-Protocol field.
+
+ See the Section 4.1., 4.2.2., and 4.3. of RFC 6455.
+ """
+
+ if not subprotocol:
+ raise HandshakeException('Invalid subprotocol name: empty')
+
+ # Parameter should be encoded HTTP token.
+ state = http_header_util.ParsingState(subprotocol)
+ token = http_header_util.consume_token(state)
+ rest = http_header_util.peek(state)
+ # If |rest| is not None, |subprotocol| is not one token or invalid. If
+ # |rest| is None, |token| must not be None because |subprotocol| is
+ # concatenation of |token| and |rest| and is not None.
+ if rest is not None:
+ raise HandshakeException('Invalid non-token string in subprotocol '
+ 'name: %r' % rest)
+
+
+def parse_host_header(request):
+ fields = request.headers_in[common.HOST_HEADER].split(':', 1)
+ if len(fields) == 1:
+ return fields[0], get_default_port(request.is_https())
+ try:
+ return fields[0], int(fields[1])
+ except ValueError, e:
+ raise HandshakeException('Invalid port number format: %r' % e)
+
+
+def format_header(name, value):
+ return '%s: %s\r\n' % (name, value)
+
+
+def get_mandatory_header(request, key):
+ value = request.headers_in.get(key)
+ if value is None:
+ raise HandshakeException('Header %s is not defined' % key)
+ return value
+
+
+def validate_mandatory_header(request, key, expected_value, fail_status=None):
+ value = get_mandatory_header(request, key)
+
+ if value.lower() != expected_value.lower():
+ raise HandshakeException(
+ 'Expected %r for header %s but found %r (case-insensitive)' %
+ (expected_value, key, value), status=fail_status)
+
+
+def check_request_line(request):
+ # 5.1 1. The three character UTF-8 string "GET".
+ # 5.1 2. A UTF-8-encoded U+0020 SPACE character (0x20 byte).
+ if request.method != 'GET':
+ raise HandshakeException('Method is not GET: %r' % request.method)
+
+ if request.protocol != 'HTTP/1.1':
+ raise HandshakeException('Version is not HTTP/1.1: %r' %
+ request.protocol)
+
+
+def parse_token_list(data):
+ """Parses a header value which follows 1#token and returns parsed elements
+ as a list of strings.
+
+ Leading LWSes must be trimmed.
+ """
+
+ state = http_header_util.ParsingState(data)
+
+ token_list = []
+
+ while True:
+ token = http_header_util.consume_token(state)
+ if token is not None:
+ token_list.append(token)
+
+ http_header_util.consume_lwses(state)
+
+ if http_header_util.peek(state) is None:
+ break
+
+ if not http_header_util.consume_string(state, ','):
+ raise HandshakeException(
+ 'Expected a comma but found %r' % http_header_util.peek(state))
+
+ http_header_util.consume_lwses(state)
+
+ if len(token_list) == 0:
+ raise HandshakeException('No valid token found')
+
+ return token_list
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/hybi.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/hybi.py
new file mode 100644
index 000000000..1ad10ea37
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/hybi.py
@@ -0,0 +1,420 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""This file provides the opening handshake processor for the WebSocket
+protocol (RFC 6455).
+
+Specification:
+http://tools.ietf.org/html/rfc6455
+"""
+
+
+# Note: request.connection.write is used in this module, even though mod_python
+# document says that it should be used only in connection handlers.
+# Unfortunately, we have no other options. For example, request.write is not
+# suitable because it doesn't allow direct raw bytes writing.
+
+
+import base64
+import logging
+import os
+import re
+
+from mod_pywebsocket import common
+from mod_pywebsocket.extensions import get_extension_processor
+from mod_pywebsocket.extensions import is_compression_extension
+from mod_pywebsocket.handshake._base import check_request_line
+from mod_pywebsocket.handshake._base import format_header
+from mod_pywebsocket.handshake._base import get_mandatory_header
+from mod_pywebsocket.handshake._base import HandshakeException
+from mod_pywebsocket.handshake._base import parse_token_list
+from mod_pywebsocket.handshake._base import validate_mandatory_header
+from mod_pywebsocket.handshake._base import validate_subprotocol
+from mod_pywebsocket.handshake._base import VersionException
+from mod_pywebsocket.stream import Stream
+from mod_pywebsocket.stream import StreamOptions
+from mod_pywebsocket import util
+
+
+# Used to validate the value in the Sec-WebSocket-Key header strictly. RFC 4648
+# disallows non-zero padding, so the character right before == must be any of
+# A, Q, g and w.
+_SEC_WEBSOCKET_KEY_REGEX = re.compile('^[+/0-9A-Za-z]{21}[AQgw]==$')
+
+# Defining aliases for values used frequently.
+_VERSION_LATEST = common.VERSION_HYBI_LATEST
+_VERSION_LATEST_STRING = str(_VERSION_LATEST)
+_SUPPORTED_VERSIONS = [
+ _VERSION_LATEST,
+]
+
+
+def compute_accept(key):
+ """Computes value for the Sec-WebSocket-Accept header from value of the
+ Sec-WebSocket-Key header.
+ """
+
+ accept_binary = util.sha1_hash(
+ key + common.WEBSOCKET_ACCEPT_UUID).digest()
+ accept = base64.b64encode(accept_binary)
+
+ return (accept, accept_binary)
+
+
+class Handshaker(object):
+ """Opening handshake processor for the WebSocket protocol (RFC 6455)."""
+
+ def __init__(self, request, dispatcher):
+ """Construct an instance.
+
+ Args:
+ request: mod_python request.
+ dispatcher: Dispatcher (dispatch.Dispatcher).
+
+ Handshaker will add attributes such as ws_resource during handshake.
+ """
+
+ self._logger = util.get_class_logger(self)
+
+ self._request = request
+ self._dispatcher = dispatcher
+
+ def _validate_connection_header(self):
+ connection = get_mandatory_header(
+ self._request, common.CONNECTION_HEADER)
+
+ try:
+ connection_tokens = parse_token_list(connection)
+ except HandshakeException, e:
+ raise HandshakeException(
+ 'Failed to parse %s: %s' % (common.CONNECTION_HEADER, e))
+
+ connection_is_valid = False
+ for token in connection_tokens:
+ if token.lower() == common.UPGRADE_CONNECTION_TYPE.lower():
+ connection_is_valid = True
+ break
+ if not connection_is_valid:
+ raise HandshakeException(
+ '%s header doesn\'t contain "%s"' %
+ (common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE))
+
+ def do_handshake(self):
+ self._request.ws_close_code = None
+ self._request.ws_close_reason = None
+
+ # Parsing.
+
+ check_request_line(self._request)
+
+ validate_mandatory_header(
+ self._request,
+ common.UPGRADE_HEADER,
+ common.WEBSOCKET_UPGRADE_TYPE)
+
+ self._validate_connection_header()
+
+ self._request.ws_resource = self._request.uri
+
+ unused_host = get_mandatory_header(self._request, common.HOST_HEADER)
+
+ self._request.ws_version = self._check_version()
+
+ try:
+ self._get_origin()
+ self._set_protocol()
+ self._parse_extensions()
+
+ # Key validation, response generation.
+
+ key = self._get_key()
+ (accept, accept_binary) = compute_accept(key)
+ self._logger.debug(
+ '%s: %r (%s)',
+ common.SEC_WEBSOCKET_ACCEPT_HEADER,
+ accept,
+ util.hexify(accept_binary))
+
+ self._logger.debug('Protocol version is RFC 6455')
+
+ # Setup extension processors.
+
+ processors = []
+ if self._request.ws_requested_extensions is not None:
+ for extension_request in self._request.ws_requested_extensions:
+ processor = get_extension_processor(extension_request)
+ # Unknown extension requests are just ignored.
+ if processor is not None:
+ processors.append(processor)
+ self._request.ws_extension_processors = processors
+
+ # List of extra headers. The extra handshake handler may add header
+ # data as name/value pairs to this list and pywebsocket appends
+ # them to the WebSocket handshake.
+ self._request.extra_headers = []
+
+ # Extra handshake handler may modify/remove processors.
+ self._dispatcher.do_extra_handshake(self._request)
+ processors = filter(lambda processor: processor is not None,
+ self._request.ws_extension_processors)
+
+ # Ask each processor if there are extensions on the request which
+ # cannot co-exist. When processor decided other processors cannot
+ # co-exist with it, the processor marks them (or itself) as
+ # "inactive". The first extension processor has the right to
+ # make the final call.
+ for processor in reversed(processors):
+ if processor.is_active():
+ processor.check_consistency_with_other_processors(
+ processors)
+ processors = filter(lambda processor: processor.is_active(),
+ processors)
+
+ accepted_extensions = []
+
+ # We need to take into account of mux extension here.
+ # If mux extension exists:
+ # - Remove processors of extensions for logical channel,
+ # which are processors located before the mux processor
+ # - Pass extension requests for logical channel to mux processor
+ # - Attach the mux processor to the request. It will be referred
+ # by dispatcher to see whether the dispatcher should use mux
+ # handler or not.
+ mux_index = -1
+ for i, processor in enumerate(processors):
+ if processor.name() == common.MUX_EXTENSION:
+ mux_index = i
+ break
+ if mux_index >= 0:
+ logical_channel_extensions = []
+ for processor in processors[:mux_index]:
+ logical_channel_extensions.append(processor.request())
+ processor.set_active(False)
+ self._request.mux_processor = processors[mux_index]
+ self._request.mux_processor.set_extensions(
+ logical_channel_extensions)
+ processors = filter(lambda processor: processor.is_active(),
+ processors)
+
+ stream_options = StreamOptions()
+
+ for index, processor in enumerate(processors):
+ if not processor.is_active():
+ continue
+
+ extension_response = processor.get_extension_response()
+ if extension_response is None:
+ # Rejected.
+ continue
+
+ accepted_extensions.append(extension_response)
+
+ processor.setup_stream_options(stream_options)
+
+ if not is_compression_extension(processor.name()):
+ continue
+
+ # Inactivate all of the following compression extensions.
+ for j in xrange(index + 1, len(processors)):
+ if is_compression_extension(processors[j].name()):
+ processors[j].set_active(False)
+
+ if len(accepted_extensions) > 0:
+ self._request.ws_extensions = accepted_extensions
+ self._logger.debug(
+ 'Extensions accepted: %r',
+ map(common.ExtensionParameter.name, accepted_extensions))
+ else:
+ self._request.ws_extensions = None
+
+ self._request.ws_stream = self._create_stream(stream_options)
+
+ if self._request.ws_requested_protocols is not None:
+ if self._request.ws_protocol is None:
+ raise HandshakeException(
+ 'do_extra_handshake must choose one subprotocol from '
+ 'ws_requested_protocols and set it to ws_protocol')
+ validate_subprotocol(self._request.ws_protocol)
+
+ self._logger.debug(
+ 'Subprotocol accepted: %r',
+ self._request.ws_protocol)
+ else:
+ if self._request.ws_protocol is not None:
+ raise HandshakeException(
+ 'ws_protocol must be None when the client didn\'t '
+ 'request any subprotocol')
+
+ self._send_handshake(accept)
+ except HandshakeException, e:
+ if not e.status:
+ # Fallback to 400 bad request by default.
+ e.status = common.HTTP_STATUS_BAD_REQUEST
+ raise e
+
+ def _get_origin(self):
+ origin_header = common.ORIGIN_HEADER
+ origin = self._request.headers_in.get(origin_header)
+ if origin is None:
+ self._logger.debug('Client request does not have origin header')
+ self._request.ws_origin = origin
+
+ def _check_version(self):
+ version = get_mandatory_header(self._request,
+ common.SEC_WEBSOCKET_VERSION_HEADER)
+ if version == _VERSION_LATEST_STRING:
+ return _VERSION_LATEST
+
+ if version.find(',') >= 0:
+ raise HandshakeException(
+ 'Multiple versions (%r) are not allowed for header %s' %
+ (version, common.SEC_WEBSOCKET_VERSION_HEADER),
+ status=common.HTTP_STATUS_BAD_REQUEST)
+ raise VersionException(
+ 'Unsupported version %r for header %s' %
+ (version, common.SEC_WEBSOCKET_VERSION_HEADER),
+ supported_versions=', '.join(map(str, _SUPPORTED_VERSIONS)))
+
+ def _set_protocol(self):
+ self._request.ws_protocol = None
+
+ protocol_header = self._request.headers_in.get(
+ common.SEC_WEBSOCKET_PROTOCOL_HEADER)
+
+ if protocol_header is None:
+ self._request.ws_requested_protocols = None
+ return
+
+ self._request.ws_requested_protocols = parse_token_list(
+ protocol_header)
+ self._logger.debug('Subprotocols requested: %r',
+ self._request.ws_requested_protocols)
+
+ def _parse_extensions(self):
+ extensions_header = self._request.headers_in.get(
+ common.SEC_WEBSOCKET_EXTENSIONS_HEADER)
+ if not extensions_header:
+ self._request.ws_requested_extensions = None
+ return
+
+ try:
+ self._request.ws_requested_extensions = common.parse_extensions(
+ extensions_header)
+ except common.ExtensionParsingException, e:
+ raise HandshakeException(
+ 'Failed to parse Sec-WebSocket-Extensions header: %r' % e)
+
+ self._logger.debug(
+ 'Extensions requested: %r',
+ map(common.ExtensionParameter.name,
+ self._request.ws_requested_extensions))
+
+ def _validate_key(self, key):
+ if key.find(',') >= 0:
+ raise HandshakeException('Request has multiple %s header lines or '
+ 'contains illegal character \',\': %r' %
+ (common.SEC_WEBSOCKET_KEY_HEADER, key))
+
+ # Validate
+ key_is_valid = False
+ try:
+ # Validate key by quick regex match before parsing by base64
+ # module. Because base64 module skips invalid characters, we have
+ # to do this in advance to make this server strictly reject illegal
+ # keys.
+ if _SEC_WEBSOCKET_KEY_REGEX.match(key):
+ decoded_key = base64.b64decode(key)
+ if len(decoded_key) == 16:
+ key_is_valid = True
+ except TypeError, e:
+ pass
+
+ if not key_is_valid:
+ raise HandshakeException(
+ 'Illegal value for header %s: %r' %
+ (common.SEC_WEBSOCKET_KEY_HEADER, key))
+
+ return decoded_key
+
+ def _get_key(self):
+ key = get_mandatory_header(
+ self._request, common.SEC_WEBSOCKET_KEY_HEADER)
+
+ decoded_key = self._validate_key(key)
+
+ self._logger.debug(
+ '%s: %r (%s)',
+ common.SEC_WEBSOCKET_KEY_HEADER,
+ key,
+ util.hexify(decoded_key))
+
+ return key
+
+ def _create_stream(self, stream_options):
+ return Stream(self._request, stream_options)
+
+ def _create_handshake_response(self, accept):
+ response = []
+
+ response.append('HTTP/1.1 101 Switching Protocols\r\n')
+
+ # WebSocket headers
+ response.append(format_header(
+ common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE))
+ response.append(format_header(
+ common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE))
+ response.append(format_header(
+ common.SEC_WEBSOCKET_ACCEPT_HEADER, accept))
+ if self._request.ws_protocol is not None:
+ response.append(format_header(
+ common.SEC_WEBSOCKET_PROTOCOL_HEADER,
+ self._request.ws_protocol))
+ if (self._request.ws_extensions is not None and
+ len(self._request.ws_extensions) != 0):
+ response.append(format_header(
+ common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
+ common.format_extensions(self._request.ws_extensions)))
+
+ # Headers not specific for WebSocket
+ for name, value in self._request.extra_headers:
+ response.append(format_header(name, value))
+
+ response.append('\r\n')
+
+ return ''.join(response)
+
+ def _send_handshake(self, accept):
+ raw_response = self._create_handshake_response(accept)
+ self._request.connection.write(raw_response)
+ self._logger.debug('Sent server\'s opening handshake: %r',
+ raw_response)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/hybi00.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/hybi00.py
new file mode 100644
index 000000000..8757717a6
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/hybi00.py
@@ -0,0 +1,293 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""This file provides the opening handshake processor for the WebSocket
+protocol version HyBi 00.
+
+Specification:
+http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00
+"""
+
+
+# Note: request.connection.write/read are used in this module, even though
+# mod_python document says that they should be used only in connection
+# handlers. Unfortunately, we have no other options. For example,
+# request.write/read are not suitable because they don't allow direct raw bytes
+# writing/reading.
+
+
+import logging
+import re
+import struct
+
+from mod_pywebsocket import common
+from mod_pywebsocket.stream import StreamHixie75
+from mod_pywebsocket import util
+from mod_pywebsocket.handshake._base import HandshakeException
+from mod_pywebsocket.handshake._base import check_request_line
+from mod_pywebsocket.handshake._base import format_header
+from mod_pywebsocket.handshake._base import get_default_port
+from mod_pywebsocket.handshake._base import get_mandatory_header
+from mod_pywebsocket.handshake._base import parse_host_header
+from mod_pywebsocket.handshake._base import validate_mandatory_header
+
+
+_MANDATORY_HEADERS = [
+ # key, expected value or None
+ [common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75],
+ [common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE],
+]
+
+
+def _validate_subprotocol(subprotocol):
+ """Checks if characters in subprotocol are in range between U+0020 and
+ U+007E. A value in the Sec-WebSocket-Protocol field need to satisfy this
+ requirement.
+
+ See the Section 4.1. Opening handshake of the spec.
+ """
+
+ if not subprotocol:
+ raise HandshakeException('Invalid subprotocol name: empty')
+
+ # Parameter should be in the range U+0020 to U+007E.
+ for c in subprotocol:
+ if not 0x20 <= ord(c) <= 0x7e:
+ raise HandshakeException(
+ 'Illegal character in subprotocol name: %r' % c)
+
+
+def _check_header_lines(request, mandatory_headers):
+ check_request_line(request)
+
+ # The expected field names, and the meaning of their corresponding
+ # values, are as follows.
+ # |Upgrade| and |Connection|
+ for key, expected_value in mandatory_headers:
+ validate_mandatory_header(request, key, expected_value)
+
+
+def _build_location(request):
+ """Build WebSocket location for request."""
+
+ location_parts = []
+ if request.is_https():
+ location_parts.append(common.WEB_SOCKET_SECURE_SCHEME)
+ else:
+ location_parts.append(common.WEB_SOCKET_SCHEME)
+ location_parts.append('://')
+ host, port = parse_host_header(request)
+ connection_port = request.connection.local_addr[1]
+ if port != connection_port:
+ raise HandshakeException('Header/connection port mismatch: %d/%d' %
+ (port, connection_port))
+ location_parts.append(host)
+ if (port != get_default_port(request.is_https())):
+ location_parts.append(':')
+ location_parts.append(str(port))
+ location_parts.append(request.unparsed_uri)
+ return ''.join(location_parts)
+
+
+class Handshaker(object):
+ """Opening handshake processor for the WebSocket protocol version HyBi 00.
+ """
+
+ def __init__(self, request, dispatcher):
+ """Construct an instance.
+
+ Args:
+ request: mod_python request.
+ dispatcher: Dispatcher (dispatch.Dispatcher).
+
+ Handshaker will add attributes such as ws_resource in performing
+ handshake.
+ """
+
+ self._logger = util.get_class_logger(self)
+
+ self._request = request
+ self._dispatcher = dispatcher
+
+ def do_handshake(self):
+ """Perform WebSocket Handshake.
+
+ On _request, we set
+ ws_resource, ws_protocol, ws_location, ws_origin, ws_challenge,
+ ws_challenge_md5: WebSocket handshake information.
+ ws_stream: Frame generation/parsing class.
+ ws_version: Protocol version.
+
+ Raises:
+ HandshakeException: when any error happened in parsing the opening
+ handshake request.
+ """
+
+ # 5.1 Reading the client's opening handshake.
+ # dispatcher sets it in self._request.
+ _check_header_lines(self._request, _MANDATORY_HEADERS)
+ self._set_resource()
+ self._set_subprotocol()
+ self._set_location()
+ self._set_origin()
+ self._set_challenge_response()
+ self._set_protocol_version()
+
+ self._dispatcher.do_extra_handshake(self._request)
+
+ self._send_handshake()
+
+ def _set_resource(self):
+ self._request.ws_resource = self._request.uri
+
+ def _set_subprotocol(self):
+ # |Sec-WebSocket-Protocol|
+ subprotocol = self._request.headers_in.get(
+ common.SEC_WEBSOCKET_PROTOCOL_HEADER)
+ if subprotocol is not None:
+ _validate_subprotocol(subprotocol)
+ self._request.ws_protocol = subprotocol
+
+ def _set_location(self):
+ # |Host|
+ host = self._request.headers_in.get(common.HOST_HEADER)
+ if host is not None:
+ self._request.ws_location = _build_location(self._request)
+ # TODO(ukai): check host is this host.
+
+ def _set_origin(self):
+ # |Origin|
+ origin = self._request.headers_in.get(common.ORIGIN_HEADER)
+ if origin is not None:
+ self._request.ws_origin = origin
+
+ def _set_protocol_version(self):
+ # |Sec-WebSocket-Draft|
+ draft = self._request.headers_in.get(common.SEC_WEBSOCKET_DRAFT_HEADER)
+ if draft is not None and draft != '0':
+ raise HandshakeException('Illegal value for %s: %s' %
+ (common.SEC_WEBSOCKET_DRAFT_HEADER,
+ draft))
+
+ self._logger.debug('Protocol version is HyBi 00')
+ self._request.ws_version = common.VERSION_HYBI00
+ self._request.ws_stream = StreamHixie75(self._request, True)
+
+ def _set_challenge_response(self):
+ # 5.2 4-8.
+ self._request.ws_challenge = self._get_challenge()
+ # 5.2 9. let /response/ be the MD5 finterprint of /challenge/
+ self._request.ws_challenge_md5 = util.md5_hash(
+ self._request.ws_challenge).digest()
+ self._logger.debug(
+ 'Challenge: %r (%s)',
+ self._request.ws_challenge,
+ util.hexify(self._request.ws_challenge))
+ self._logger.debug(
+ 'Challenge response: %r (%s)',
+ self._request.ws_challenge_md5,
+ util.hexify(self._request.ws_challenge_md5))
+
+ def _get_key_value(self, key_field):
+ key_value = get_mandatory_header(self._request, key_field)
+
+ self._logger.debug('%s: %r', key_field, key_value)
+
+ # 5.2 4. let /key-number_n/ be the digits (characters in the range
+ # U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9)) in /key_n/,
+ # interpreted as a base ten integer, ignoring all other characters
+ # in /key_n/.
+ try:
+ key_number = int(re.sub("\\D", "", key_value))
+ except:
+ raise HandshakeException('%s field contains no digit' % key_field)
+ # 5.2 5. let /spaces_n/ be the number of U+0020 SPACE characters
+ # in /key_n/.
+ spaces = re.subn(" ", "", key_value)[1]
+ if spaces == 0:
+ raise HandshakeException('%s field contains no space' % key_field)
+
+ self._logger.debug(
+ '%s: Key-number is %d and number of spaces is %d',
+ key_field, key_number, spaces)
+
+ # 5.2 6. if /key-number_n/ is not an integral multiple of /spaces_n/
+ # then abort the WebSocket connection.
+ if key_number % spaces != 0:
+ raise HandshakeException(
+ '%s: Key-number (%d) is not an integral multiple of spaces '
+ '(%d)' % (key_field, key_number, spaces))
+ # 5.2 7. let /part_n/ be /key-number_n/ divided by /spaces_n/.
+ part = key_number / spaces
+ self._logger.debug('%s: Part is %d', key_field, part)
+ return part
+
+ def _get_challenge(self):
+ # 5.2 4-7.
+ key1 = self._get_key_value(common.SEC_WEBSOCKET_KEY1_HEADER)
+ key2 = self._get_key_value(common.SEC_WEBSOCKET_KEY2_HEADER)
+ # 5.2 8. let /challenge/ be the concatenation of /part_1/,
+ challenge = ''
+ challenge += struct.pack('!I', key1) # network byteorder int
+ challenge += struct.pack('!I', key2) # network byteorder int
+ challenge += self._request.connection.read(8)
+ return challenge
+
+ def _send_handshake(self):
+ response = []
+
+ # 5.2 10. send the following line.
+ response.append('HTTP/1.1 101 WebSocket Protocol Handshake\r\n')
+
+ # 5.2 11. send the following fields to the client.
+ response.append(format_header(
+ common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75))
+ response.append(format_header(
+ common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE))
+ response.append(format_header(
+ common.SEC_WEBSOCKET_LOCATION_HEADER, self._request.ws_location))
+ response.append(format_header(
+ common.SEC_WEBSOCKET_ORIGIN_HEADER, self._request.ws_origin))
+ if self._request.ws_protocol:
+ response.append(format_header(
+ common.SEC_WEBSOCKET_PROTOCOL_HEADER,
+ self._request.ws_protocol))
+ # 5.2 12. send two bytes 0x0D 0x0A.
+ response.append('\r\n')
+ # 5.2 13. send /response/
+ response.append(self._request.ws_challenge_md5)
+
+ raw_response = ''.join(response)
+ self._request.connection.write(raw_response)
+ self._logger.debug('Sent server\'s opening handshake: %r',
+ raw_response)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/headerparserhandler.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/headerparserhandler.py
new file mode 100644
index 000000000..c244421cf
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/headerparserhandler.py
@@ -0,0 +1,254 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""PythonHeaderParserHandler for mod_pywebsocket.
+
+Apache HTTP Server and mod_python must be configured such that this
+function is called to handle WebSocket request.
+"""
+
+
+import logging
+
+from mod_python import apache
+
+from mod_pywebsocket import common
+from mod_pywebsocket import dispatch
+from mod_pywebsocket import handshake
+from mod_pywebsocket import util
+
+
+# PythonOption to specify the handler root directory.
+_PYOPT_HANDLER_ROOT = 'mod_pywebsocket.handler_root'
+
+# PythonOption to specify the handler scan directory.
+# This must be a directory under the root directory.
+# The default is the root directory.
+_PYOPT_HANDLER_SCAN = 'mod_pywebsocket.handler_scan'
+
+# PythonOption to allow handlers whose canonical path is
+# not under the root directory. It's disallowed by default.
+# Set this option with value of 'yes' to allow.
+_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT = (
+ 'mod_pywebsocket.allow_handlers_outside_root_dir')
+# Map from values to their meanings. 'Yes' and 'No' are allowed just for
+# compatibility.
+_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT_DEFINITION = {
+ 'off': False, 'no': False, 'on': True, 'yes': True}
+
+# (Obsolete option. Ignored.)
+# PythonOption to specify to allow handshake defined in Hixie 75 version
+# protocol. The default is None (Off)
+_PYOPT_ALLOW_DRAFT75 = 'mod_pywebsocket.allow_draft75'
+# Map from values to their meanings.
+_PYOPT_ALLOW_DRAFT75_DEFINITION = {'off': False, 'on': True}
+
+
+class ApacheLogHandler(logging.Handler):
+ """Wrapper logging.Handler to emit log message to apache's error.log."""
+
+ _LEVELS = {
+ logging.DEBUG: apache.APLOG_DEBUG,
+ logging.INFO: apache.APLOG_INFO,
+ logging.WARNING: apache.APLOG_WARNING,
+ logging.ERROR: apache.APLOG_ERR,
+ logging.CRITICAL: apache.APLOG_CRIT,
+ }
+
+ def __init__(self, request=None):
+ logging.Handler.__init__(self)
+ self._log_error = apache.log_error
+ if request is not None:
+ self._log_error = request.log_error
+
+ # Time and level will be printed by Apache.
+ self._formatter = logging.Formatter('%(name)s: %(message)s')
+
+ def emit(self, record):
+ apache_level = apache.APLOG_DEBUG
+ if record.levelno in ApacheLogHandler._LEVELS:
+ apache_level = ApacheLogHandler._LEVELS[record.levelno]
+
+ msg = self._formatter.format(record)
+
+ # "server" parameter must be passed to have "level" parameter work.
+ # If only "level" parameter is passed, nothing shows up on Apache's
+ # log. However, at this point, we cannot get the server object of the
+ # virtual host which will process WebSocket requests. The only server
+ # object we can get here is apache.main_server. But Wherever (server
+ # configuration context or virtual host context) we put
+ # PythonHeaderParserHandler directive, apache.main_server just points
+ # the main server instance (not any of virtual server instance). Then,
+ # Apache follows LogLevel directive in the server configuration context
+ # to filter logs. So, we need to specify LogLevel in the server
+ # configuration context. Even if we specify "LogLevel debug" in the
+ # virtual host context which actually handles WebSocket connections,
+ # DEBUG level logs never show up unless "LogLevel debug" is specified
+ # in the server configuration context.
+ #
+ # TODO(tyoshino): Provide logging methods on request object. When
+ # request is mp_request object (when used together with Apache), the
+ # methods call request.log_error indirectly. When request is
+ # _StandaloneRequest, the methods call Python's logging facility which
+ # we create in standalone.py.
+ self._log_error(msg, apache_level, apache.main_server)
+
+
+def _configure_logging():
+ logger = logging.getLogger()
+ # Logs are filtered by Apache based on LogLevel directive in Apache
+ # configuration file. We must just pass logs for all levels to
+ # ApacheLogHandler.
+ logger.setLevel(logging.DEBUG)
+ logger.addHandler(ApacheLogHandler())
+
+
+_configure_logging()
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def _parse_option(name, value, definition):
+ if value is None:
+ return False
+
+ meaning = definition.get(value.lower())
+ if meaning is None:
+ raise Exception('Invalid value for PythonOption %s: %r' %
+ (name, value))
+ return meaning
+
+
+def _create_dispatcher():
+ _LOGGER.info('Initializing Dispatcher')
+
+ options = apache.main_server.get_options()
+
+ handler_root = options.get(_PYOPT_HANDLER_ROOT, None)
+ if not handler_root:
+ raise Exception('PythonOption %s is not defined' % _PYOPT_HANDLER_ROOT,
+ apache.APLOG_ERR)
+
+ handler_scan = options.get(_PYOPT_HANDLER_SCAN, handler_root)
+
+ allow_handlers_outside_root = _parse_option(
+ _PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT,
+ options.get(_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT),
+ _PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT_DEFINITION)
+
+ dispatcher = dispatch.Dispatcher(
+ handler_root, handler_scan, allow_handlers_outside_root)
+
+ for warning in dispatcher.source_warnings():
+ apache.log_error(
+ 'mod_pywebsocket: Warning in source loading: %s' % warning,
+ apache.APLOG_WARNING)
+
+ return dispatcher
+
+
+# Initialize
+_dispatcher = _create_dispatcher()
+
+
+def headerparserhandler(request):
+ """Handle request.
+
+ Args:
+ request: mod_python request.
+
+ This function is named headerparserhandler because it is the default
+ name for a PythonHeaderParserHandler.
+ """
+
+ handshake_is_done = False
+ try:
+ # Fallback to default http handler for request paths for which
+ # we don't have request handlers.
+ if not _dispatcher.get_handler_suite(request.uri):
+ request.log_error(
+ 'mod_pywebsocket: No handler for resource: %r' % request.uri,
+ apache.APLOG_INFO)
+ request.log_error(
+ 'mod_pywebsocket: Fallback to Apache', apache.APLOG_INFO)
+ return apache.DECLINED
+ except dispatch.DispatchException, e:
+ request.log_error(
+ 'mod_pywebsocket: Dispatch failed for error: %s' % e,
+ apache.APLOG_INFO)
+ if not handshake_is_done:
+ return e.status
+
+ try:
+ allow_draft75 = _parse_option(
+ _PYOPT_ALLOW_DRAFT75,
+ apache.main_server.get_options().get(_PYOPT_ALLOW_DRAFT75),
+ _PYOPT_ALLOW_DRAFT75_DEFINITION)
+
+ try:
+ handshake.do_handshake(
+ request, _dispatcher, allowDraft75=allow_draft75)
+ except handshake.VersionException, e:
+ request.log_error(
+ 'mod_pywebsocket: Handshake failed for version error: %s' % e,
+ apache.APLOG_INFO)
+ request.err_headers_out.add(common.SEC_WEBSOCKET_VERSION_HEADER,
+ e.supported_versions)
+ return apache.HTTP_BAD_REQUEST
+ except handshake.HandshakeException, e:
+ # Handshake for ws/wss failed.
+ # Send http response with error status.
+ request.log_error(
+ 'mod_pywebsocket: Handshake failed for error: %s' % e,
+ apache.APLOG_INFO)
+ return e.status
+
+ handshake_is_done = True
+ request._dispatcher = _dispatcher
+ _dispatcher.transfer_data(request)
+ except handshake.AbortedByUserException, e:
+ request.log_error('mod_pywebsocket: Aborted: %s' % e, apache.APLOG_INFO)
+ except Exception, e:
+ # DispatchException can also be thrown if something is wrong in
+ # pywebsocket code. It's caught here, then.
+
+ request.log_error('mod_pywebsocket: Exception occurred: %s\n%s' %
+ (e, util.get_stack_trace()),
+ apache.APLOG_ERR)
+ # Unknown exceptions before handshake mean Apache must handle its
+ # request with another handler.
+ if not handshake_is_done:
+ return apache.DECLINED
+ # Set assbackwards to suppress response header generation by Apache.
+ request.assbackwards = 1
+ return apache.DONE # Return DONE such that no other handlers are invoked.
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/http_header_util.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/http_header_util.py
new file mode 100644
index 000000000..b77465393
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/http_header_util.py
@@ -0,0 +1,263 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Utilities for parsing and formatting headers that follow the grammar defined
+in HTTP RFC http://www.ietf.org/rfc/rfc2616.txt.
+"""
+
+
+import urlparse
+
+
+_SEPARATORS = '()<>@,;:\\"/[]?={} \t'
+
+
+def _is_char(c):
+ """Returns true iff c is in CHAR as specified in HTTP RFC."""
+
+ return ord(c) <= 127
+
+
+def _is_ctl(c):
+ """Returns true iff c is in CTL as specified in HTTP RFC."""
+
+ return ord(c) <= 31 or ord(c) == 127
+
+
+class ParsingState(object):
+
+ def __init__(self, data):
+ self.data = data
+ self.head = 0
+
+
+def peek(state, pos=0):
+ """Peeks the character at pos from the head of data."""
+
+ if state.head + pos >= len(state.data):
+ return None
+
+ return state.data[state.head + pos]
+
+
+def consume(state, amount=1):
+ """Consumes specified amount of bytes from the head and returns the
+ consumed bytes. If there's not enough bytes to consume, returns None.
+ """
+
+ if state.head + amount > len(state.data):
+ return None
+
+ result = state.data[state.head:state.head + amount]
+ state.head = state.head + amount
+ return result
+
+
+def consume_string(state, expected):
+ """Given a parsing state and a expected string, consumes the string from
+ the head. Returns True if consumed successfully. Otherwise, returns
+ False.
+ """
+
+ pos = 0
+
+ for c in expected:
+ if c != peek(state, pos):
+ return False
+ pos += 1
+
+ consume(state, pos)
+ return True
+
+
+def consume_lws(state):
+ """Consumes a LWS from the head. Returns True if any LWS is consumed.
+ Otherwise, returns False.
+
+ LWS = [CRLF] 1*( SP | HT )
+ """
+
+ original_head = state.head
+
+ consume_string(state, '\r\n')
+
+ pos = 0
+
+ while True:
+ c = peek(state, pos)
+ if c == ' ' or c == '\t':
+ pos += 1
+ else:
+ if pos == 0:
+ state.head = original_head
+ return False
+ else:
+ consume(state, pos)
+ return True
+
+
+def consume_lwses(state):
+ """Consumes *LWS from the head."""
+
+ while consume_lws(state):
+ pass
+
+
+def consume_token(state):
+ """Consumes a token from the head. Returns the token or None if no token
+ was found.
+ """
+
+ pos = 0
+
+ while True:
+ c = peek(state, pos)
+ if c is None or c in _SEPARATORS or _is_ctl(c) or not _is_char(c):
+ if pos == 0:
+ return None
+
+ return consume(state, pos)
+ else:
+ pos += 1
+
+
+def consume_token_or_quoted_string(state):
+ """Consumes a token or a quoted-string, and returns the token or unquoted
+ string. If no token or quoted-string was found, returns None.
+ """
+
+ original_head = state.head
+
+ if not consume_string(state, '"'):
+ return consume_token(state)
+
+ result = []
+
+ expect_quoted_pair = False
+
+ while True:
+ if not expect_quoted_pair and consume_lws(state):
+ result.append(' ')
+ continue
+
+ c = consume(state)
+ if c is None:
+ # quoted-string is not enclosed with double quotation
+ state.head = original_head
+ return None
+ elif expect_quoted_pair:
+ expect_quoted_pair = False
+ if _is_char(c):
+ result.append(c)
+ else:
+ # Non CHAR character found in quoted-pair
+ state.head = original_head
+ return None
+ elif c == '\\':
+ expect_quoted_pair = True
+ elif c == '"':
+ return ''.join(result)
+ elif _is_ctl(c):
+ # Invalid character %r found in qdtext
+ state.head = original_head
+ return None
+ else:
+ result.append(c)
+
+
+def quote_if_necessary(s):
+ """Quotes arbitrary string into quoted-string."""
+
+ quote = False
+ if s == '':
+ return '""'
+
+ result = []
+ for c in s:
+ if c == '"' or c in _SEPARATORS or _is_ctl(c) or not _is_char(c):
+ quote = True
+
+ if c == '"' or _is_ctl(c):
+ result.append('\\' + c)
+ else:
+ result.append(c)
+
+ if quote:
+ return '"' + ''.join(result) + '"'
+ else:
+ return ''.join(result)
+
+
+def parse_uri(uri):
+ """Parse absolute URI then return host, port and resource."""
+
+ parsed = urlparse.urlsplit(uri)
+ if parsed.scheme != 'wss' and parsed.scheme != 'ws':
+ # |uri| must be a relative URI.
+ # TODO(toyoshim): Should validate |uri|.
+ return None, None, uri
+
+ if parsed.hostname is None:
+ return None, None, None
+
+ port = None
+ try:
+ port = parsed.port
+ except ValueError, e:
+ # port property cause ValueError on invalid null port description like
+ # 'ws://host:/path'.
+ return None, None, None
+
+ if port is None:
+ if parsed.scheme == 'ws':
+ port = 80
+ else:
+ port = 443
+
+ path = parsed.path
+ if not path:
+ path += '/'
+ if parsed.query:
+ path += '?' + parsed.query
+ if parsed.fragment:
+ path += '#' + parsed.fragment
+
+ return parsed.hostname, port, path
+
+
+try:
+ urlparse.uses_netloc.index('ws')
+except ValueError, e:
+ # urlparse in Python2.5.1 doesn't have 'ws' and 'wss' entries.
+ urlparse.uses_netloc.append('ws')
+ urlparse.uses_netloc.append('wss')
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/memorizingfile.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/memorizingfile.py
new file mode 100644
index 000000000..4d4cd9585
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/memorizingfile.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Memorizing file.
+
+A memorizing file wraps a file and memorizes lines read by readline.
+"""
+
+
+import sys
+
+
+class MemorizingFile(object):
+ """MemorizingFile wraps a file and memorizes lines read by readline.
+
+ Note that data read by other methods are not memorized. This behavior
+ is good enough for memorizing lines SimpleHTTPServer reads before
+ the control reaches WebSocketRequestHandler.
+ """
+
+ def __init__(self, file_, max_memorized_lines=sys.maxint):
+ """Construct an instance.
+
+ Args:
+ file_: the file object to wrap.
+ max_memorized_lines: the maximum number of lines to memorize.
+ Only the first max_memorized_lines are memorized.
+ Default: sys.maxint.
+ """
+
+ self._file = file_
+ self._memorized_lines = []
+ self._max_memorized_lines = max_memorized_lines
+ self._buffered = False
+ self._buffered_line = None
+
+ def __getattribute__(self, name):
+ if name in ('_file', '_memorized_lines', '_max_memorized_lines',
+ '_buffered', '_buffered_line', 'readline',
+ 'get_memorized_lines'):
+ return object.__getattribute__(self, name)
+ return self._file.__getattribute__(name)
+
+ def readline(self, size=-1):
+ """Override file.readline and memorize the line read.
+
+ Note that even if size is specified and smaller than actual size,
+ the whole line will be read out from underlying file object by
+ subsequent readline calls.
+ """
+
+ if self._buffered:
+ line = self._buffered_line
+ self._buffered = False
+ else:
+ line = self._file.readline()
+ if line and len(self._memorized_lines) < self._max_memorized_lines:
+ self._memorized_lines.append(line)
+ if size >= 0 and size < len(line):
+ self._buffered = True
+ self._buffered_line = line[size:]
+ return line[:size]
+ return line
+
+ def get_memorized_lines(self):
+ """Get lines memorized so far."""
+ return self._memorized_lines
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/msgutil.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/msgutil.py
new file mode 100644
index 000000000..4c1a0114b
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/msgutil.py
@@ -0,0 +1,219 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Message related utilities.
+
+Note: request.connection.write/read are used in this module, even though
+mod_python document says that they should be used only in connection
+handlers. Unfortunately, we have no other options. For example,
+request.write/read are not suitable because they don't allow direct raw
+bytes writing/reading.
+"""
+
+
+import Queue
+import threading
+
+
+# Export Exception symbols from msgutil for backward compatibility
+from mod_pywebsocket._stream_base import ConnectionTerminatedException
+from mod_pywebsocket._stream_base import InvalidFrameException
+from mod_pywebsocket._stream_base import BadOperationException
+from mod_pywebsocket._stream_base import UnsupportedFrameException
+
+
+# An API for handler to send/receive WebSocket messages.
+def close_connection(request):
+ """Close connection.
+
+ Args:
+ request: mod_python request.
+ """
+ request.ws_stream.close_connection()
+
+
+def send_message(request, payload_data, end=True, binary=False):
+ """Send a message (or part of a message).
+
+ Args:
+ request: mod_python request.
+ payload_data: unicode text or str binary to send.
+ end: True to terminate a message.
+ False to send payload_data as part of a message that is to be
+ terminated by next or later send_message call with end=True.
+ binary: send payload_data as binary frame(s).
+ Raises:
+ BadOperationException: when server already terminated.
+ """
+ request.ws_stream.send_message(payload_data, end, binary)
+
+
+def receive_message(request):
+ """Receive a WebSocket frame and return its payload as a text in
+ unicode or a binary in str.
+
+ Args:
+ request: mod_python request.
+ Raises:
+ InvalidFrameException: when client send invalid frame.
+ UnsupportedFrameException: when client send unsupported frame e.g. some
+ of reserved bit is set but no extension can
+ recognize it.
+ InvalidUTF8Exception: when client send a text frame containing any
+ invalid UTF-8 string.
+ ConnectionTerminatedException: when the connection is closed
+ unexpectedly.
+ BadOperationException: when client already terminated.
+ """
+ return request.ws_stream.receive_message()
+
+
+def send_ping(request, body=''):
+ request.ws_stream.send_ping(body)
+
+
+class MessageReceiver(threading.Thread):
+ """This class receives messages from the client.
+
+ This class provides three ways to receive messages: blocking,
+ non-blocking, and via callback. Callback has the highest precedence.
+
+ Note: This class should not be used with the standalone server for wss
+ because pyOpenSSL used by the server raises a fatal error if the socket
+ is accessed from multiple threads.
+ """
+
+ def __init__(self, request, onmessage=None):
+ """Construct an instance.
+
+ Args:
+ request: mod_python request.
+ onmessage: a function to be called when a message is received.
+ May be None. If not None, the function is called on
+ another thread. In that case, MessageReceiver.receive
+ and MessageReceiver.receive_nowait are useless
+ because they will never return any messages.
+ """
+
+ threading.Thread.__init__(self)
+ self._request = request
+ self._queue = Queue.Queue()
+ self._onmessage = onmessage
+ self._stop_requested = False
+ self.setDaemon(True)
+ self.start()
+
+ def run(self):
+ try:
+ while not self._stop_requested:
+ message = receive_message(self._request)
+ if self._onmessage:
+ self._onmessage(message)
+ else:
+ self._queue.put(message)
+ finally:
+ close_connection(self._request)
+
+ def receive(self):
+ """ Receive a message from the channel, blocking.
+
+ Returns:
+ message as a unicode string.
+ """
+ return self._queue.get()
+
+ def receive_nowait(self):
+ """ Receive a message from the channel, non-blocking.
+
+ Returns:
+ message as a unicode string if available. None otherwise.
+ """
+ try:
+ message = self._queue.get_nowait()
+ except Queue.Empty:
+ message = None
+ return message
+
+ def stop(self):
+ """Request to stop this instance.
+
+ The instance will be stopped after receiving the next message.
+ This method may not be very useful, but there is no clean way
+ in Python to forcefully stop a running thread.
+ """
+ self._stop_requested = True
+
+
+class MessageSender(threading.Thread):
+ """This class sends messages to the client.
+
+ This class provides both synchronous and asynchronous ways to send
+ messages.
+
+ Note: This class should not be used with the standalone server for wss
+ because pyOpenSSL used by the server raises a fatal error if the socket
+ is accessed from multiple threads.
+ """
+
+ def __init__(self, request):
+ """Construct an instance.
+
+ Args:
+ request: mod_python request.
+ """
+ threading.Thread.__init__(self)
+ self._request = request
+ self._queue = Queue.Queue()
+ self.setDaemon(True)
+ self.start()
+
+ def run(self):
+ while True:
+ message, condition = self._queue.get()
+ condition.acquire()
+ send_message(self._request, message)
+ condition.notify()
+ condition.release()
+
+ def send(self, message):
+ """Send a message, blocking."""
+
+ condition = threading.Condition()
+ condition.acquire()
+ self._queue.put((message, condition))
+ condition.wait()
+
+ def send_nowait(self, message):
+ """Send a message, non-blocking."""
+
+ self._queue.put((message, threading.Condition()))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/mux.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/mux.py
new file mode 100644
index 000000000..76334685b
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/mux.py
@@ -0,0 +1,1889 @@
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""This file provides classes and helper functions for multiplexing extension.
+
+Specification:
+http://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-06
+"""
+
+
+import collections
+import copy
+import email
+import email.parser
+import logging
+import math
+import struct
+import threading
+import traceback
+
+from mod_pywebsocket import common
+from mod_pywebsocket import handshake
+from mod_pywebsocket import util
+from mod_pywebsocket._stream_base import BadOperationException
+from mod_pywebsocket._stream_base import ConnectionTerminatedException
+from mod_pywebsocket._stream_base import InvalidFrameException
+from mod_pywebsocket._stream_hybi import Frame
+from mod_pywebsocket._stream_hybi import Stream
+from mod_pywebsocket._stream_hybi import StreamOptions
+from mod_pywebsocket._stream_hybi import create_binary_frame
+from mod_pywebsocket._stream_hybi import create_closing_handshake_body
+from mod_pywebsocket._stream_hybi import create_header
+from mod_pywebsocket._stream_hybi import create_length_header
+from mod_pywebsocket._stream_hybi import parse_frame
+from mod_pywebsocket.handshake import hybi
+
+
+_CONTROL_CHANNEL_ID = 0
+_DEFAULT_CHANNEL_ID = 1
+
+_MUX_OPCODE_ADD_CHANNEL_REQUEST = 0
+_MUX_OPCODE_ADD_CHANNEL_RESPONSE = 1
+_MUX_OPCODE_FLOW_CONTROL = 2
+_MUX_OPCODE_DROP_CHANNEL = 3
+_MUX_OPCODE_NEW_CHANNEL_SLOT = 4
+
+_MAX_CHANNEL_ID = 2 ** 29 - 1
+
+_INITIAL_NUMBER_OF_CHANNEL_SLOTS = 64
+_INITIAL_QUOTA_FOR_CLIENT = 8 * 1024
+
+_HANDSHAKE_ENCODING_IDENTITY = 0
+_HANDSHAKE_ENCODING_DELTA = 1
+
+# We need only these status code for now.
+_HTTP_BAD_RESPONSE_MESSAGES = {
+ common.HTTP_STATUS_BAD_REQUEST: 'Bad Request',
+}
+
+# DropChannel reason code
+# TODO(bashi): Define all reason code defined in -05 draft.
+_DROP_CODE_NORMAL_CLOSURE = 1000
+
+_DROP_CODE_INVALID_ENCAPSULATING_MESSAGE = 2001
+_DROP_CODE_CHANNEL_ID_TRUNCATED = 2002
+_DROP_CODE_ENCAPSULATED_FRAME_IS_TRUNCATED = 2003
+_DROP_CODE_UNKNOWN_MUX_OPCODE = 2004
+_DROP_CODE_INVALID_MUX_CONTROL_BLOCK = 2005
+_DROP_CODE_CHANNEL_ALREADY_EXISTS = 2006
+_DROP_CODE_NEW_CHANNEL_SLOT_VIOLATION = 2007
+_DROP_CODE_UNKNOWN_REQUEST_ENCODING = 2010
+
+_DROP_CODE_SEND_QUOTA_VIOLATION = 3005
+_DROP_CODE_SEND_QUOTA_OVERFLOW = 3006
+_DROP_CODE_ACKNOWLEDGED = 3008
+_DROP_CODE_BAD_FRAGMENTATION = 3009
+
+
+class MuxUnexpectedException(Exception):
+ """Exception in handling multiplexing extension."""
+ pass
+
+
+# Temporary
+class MuxNotImplementedException(Exception):
+ """Raised when a flow enters unimplemented code path."""
+ pass
+
+
+class LogicalConnectionClosedException(Exception):
+ """Raised when logical connection is gracefully closed."""
+ pass
+
+
+class PhysicalConnectionError(Exception):
+ """Raised when there is a physical connection error."""
+ def __init__(self, drop_code, message=''):
+ super(PhysicalConnectionError, self).__init__(
+ 'code=%d, message=%r' % (drop_code, message))
+ self.drop_code = drop_code
+ self.message = message
+
+
+class LogicalChannelError(Exception):
+ """Raised when there is a logical channel error."""
+ def __init__(self, channel_id, drop_code, message=''):
+ super(LogicalChannelError, self).__init__(
+ 'channel_id=%d, code=%d, message=%r' % (
+ channel_id, drop_code, message))
+ self.channel_id = channel_id
+ self.drop_code = drop_code
+ self.message = message
+
+
+def _encode_channel_id(channel_id):
+ if channel_id < 0:
+ raise ValueError('Channel id %d must not be negative' % channel_id)
+
+ if channel_id < 2 ** 7:
+ return chr(channel_id)
+ if channel_id < 2 ** 14:
+ return struct.pack('!H', 0x8000 + channel_id)
+ if channel_id < 2 ** 21:
+ first = chr(0xc0 + (channel_id >> 16))
+ return first + struct.pack('!H', channel_id & 0xffff)
+ if channel_id < 2 ** 29:
+ return struct.pack('!L', 0xe0000000 + channel_id)
+
+ raise ValueError('Channel id %d is too large' % channel_id)
+
+
+def _encode_number(number):
+ return create_length_header(number, False)
+
+
+def _create_add_channel_response(channel_id, encoded_handshake,
+ encoding=0, rejected=False):
+ if encoding != 0 and encoding != 1:
+ raise ValueError('Invalid encoding %d' % encoding)
+
+ first_byte = ((_MUX_OPCODE_ADD_CHANNEL_RESPONSE << 5) |
+ (rejected << 4) | encoding)
+ block = (chr(first_byte) +
+ _encode_channel_id(channel_id) +
+ _encode_number(len(encoded_handshake)) +
+ encoded_handshake)
+ return block
+
+
+def _create_drop_channel(channel_id, code=None, message=''):
+ if len(message) > 0 and code is None:
+ raise ValueError('Code must be specified if message is specified')
+
+ first_byte = _MUX_OPCODE_DROP_CHANNEL << 5
+ block = chr(first_byte) + _encode_channel_id(channel_id)
+ if code is None:
+ block += _encode_number(0) # Reason size
+ else:
+ reason = struct.pack('!H', code) + message
+ reason_size = _encode_number(len(reason))
+ block += reason_size + reason
+
+ return block
+
+
+def _create_flow_control(channel_id, replenished_quota):
+ first_byte = _MUX_OPCODE_FLOW_CONTROL << 5
+ block = (chr(first_byte) +
+ _encode_channel_id(channel_id) +
+ _encode_number(replenished_quota))
+ return block
+
+
+def _create_new_channel_slot(slots, send_quota):
+ if slots < 0 or send_quota < 0:
+ raise ValueError('slots and send_quota must be non-negative.')
+ first_byte = _MUX_OPCODE_NEW_CHANNEL_SLOT << 5
+ block = (chr(first_byte) +
+ _encode_number(slots) +
+ _encode_number(send_quota))
+ return block
+
+
+def _create_fallback_new_channel_slot():
+ first_byte = (_MUX_OPCODE_NEW_CHANNEL_SLOT << 5) | 1 # Set the F flag
+ block = (chr(first_byte) + _encode_number(0) + _encode_number(0))
+ return block
+
+
+def _parse_request_text(request_text):
+ request_line, header_lines = request_text.split('\r\n', 1)
+
+ words = request_line.split(' ')
+ if len(words) != 3:
+ raise ValueError('Bad Request-Line syntax %r' % request_line)
+ [command, path, version] = words
+ if version != 'HTTP/1.1':
+ raise ValueError('Bad request version %r' % version)
+
+ # email.parser.Parser() parses RFC 2822 (RFC 822) style headers.
+ # RFC 6455 refers RFC 2616 for handshake parsing, and RFC 2616 refers
+ # RFC 822.
+ headers = email.parser.Parser().parsestr(header_lines)
+ return command, path, version, headers
+
+
+class _ControlBlock(object):
+ """A structure that holds parsing result of multiplexing control block.
+ Control block specific attributes will be added by _MuxFramePayloadParser.
+ (e.g. encoded_handshake will be added for AddChannelRequest and
+ AddChannelResponse)
+ """
+
+ def __init__(self, opcode):
+ self.opcode = opcode
+
+
+class _MuxFramePayloadParser(object):
+ """A class that parses multiplexed frame payload."""
+
+ def __init__(self, payload):
+ self._data = payload
+ self._read_position = 0
+ self._logger = util.get_class_logger(self)
+
+ def read_channel_id(self):
+ """Reads channel id.
+
+ Raises:
+ ValueError: when the payload doesn't contain
+ valid channel id.
+ """
+
+ remaining_length = len(self._data) - self._read_position
+ pos = self._read_position
+ if remaining_length == 0:
+ raise ValueError('Invalid channel id format')
+
+ channel_id = ord(self._data[pos])
+ channel_id_length = 1
+ if channel_id & 0xe0 == 0xe0:
+ if remaining_length < 4:
+ raise ValueError('Invalid channel id format')
+ channel_id = struct.unpack('!L',
+ self._data[pos:pos+4])[0] & 0x1fffffff
+ channel_id_length = 4
+ elif channel_id & 0xc0 == 0xc0:
+ if remaining_length < 3:
+ raise ValueError('Invalid channel id format')
+ channel_id = (((channel_id & 0x1f) << 16) +
+ struct.unpack('!H', self._data[pos+1:pos+3])[0])
+ channel_id_length = 3
+ elif channel_id & 0x80 == 0x80:
+ if remaining_length < 2:
+ raise ValueError('Invalid channel id format')
+ channel_id = struct.unpack('!H',
+ self._data[pos:pos+2])[0] & 0x3fff
+ channel_id_length = 2
+ self._read_position += channel_id_length
+
+ return channel_id
+
+ def read_inner_frame(self):
+ """Reads an inner frame.
+
+ Raises:
+ PhysicalConnectionError: when the inner frame is invalid.
+ """
+
+ if len(self._data) == self._read_position:
+ raise PhysicalConnectionError(
+ _DROP_CODE_ENCAPSULATED_FRAME_IS_TRUNCATED)
+
+ bits = ord(self._data[self._read_position])
+ self._read_position += 1
+ fin = (bits & 0x80) == 0x80
+ rsv1 = (bits & 0x40) == 0x40
+ rsv2 = (bits & 0x20) == 0x20
+ rsv3 = (bits & 0x10) == 0x10
+ opcode = bits & 0xf
+ payload = self.remaining_data()
+ # Consume rest of the message which is payload data of the original
+ # frame.
+ self._read_position = len(self._data)
+ return fin, rsv1, rsv2, rsv3, opcode, payload
+
+ def _read_number(self):
+ if self._read_position + 1 > len(self._data):
+ raise ValueError(
+ 'Cannot read the first byte of number field')
+
+ number = ord(self._data[self._read_position])
+ if number & 0x80 == 0x80:
+ raise ValueError(
+ 'The most significant bit of the first byte of number should '
+ 'be unset')
+ self._read_position += 1
+ pos = self._read_position
+ if number == 127:
+ if pos + 8 > len(self._data):
+ raise ValueError('Invalid number field')
+ self._read_position += 8
+ number = struct.unpack('!Q', self._data[pos:pos+8])[0]
+ if number > 0x7FFFFFFFFFFFFFFF:
+ raise ValueError('Encoded number(%d) >= 2^63' % number)
+ if number <= 0xFFFF:
+ raise ValueError(
+ '%d should not be encoded by 9 bytes encoding' % number)
+ return number
+ if number == 126:
+ if pos + 2 > len(self._data):
+ raise ValueError('Invalid number field')
+ self._read_position += 2
+ number = struct.unpack('!H', self._data[pos:pos+2])[0]
+ if number <= 125:
+ raise ValueError(
+ '%d should not be encoded by 3 bytes encoding' % number)
+ return number
+
+ def _read_size_and_contents(self):
+ """Reads data that consists of followings:
+ - the size of the contents encoded the same way as payload length
+ of the WebSocket Protocol with 1 bit padding at the head.
+ - the contents.
+ """
+
+ try:
+ size = self._read_number()
+ except ValueError, e:
+ raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+ str(e))
+ pos = self._read_position
+ if pos + size > len(self._data):
+ raise PhysicalConnectionError(
+ _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+ 'Cannot read %d bytes data' % size)
+
+ self._read_position += size
+ return self._data[pos:pos+size]
+
+ def _read_add_channel_request(self, first_byte, control_block):
+ reserved = (first_byte >> 2) & 0x7
+ if reserved != 0:
+ raise PhysicalConnectionError(
+ _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+ 'Reserved bits must be unset')
+
+ # Invalid encoding will be handled by MuxHandler.
+ encoding = first_byte & 0x3
+ try:
+ control_block.channel_id = self.read_channel_id()
+ except ValueError, e:
+ raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK)
+ control_block.encoding = encoding
+ encoded_handshake = self._read_size_and_contents()
+ control_block.encoded_handshake = encoded_handshake
+ return control_block
+
+ def _read_add_channel_response(self, first_byte, control_block):
+ reserved = (first_byte >> 2) & 0x3
+ if reserved != 0:
+ raise PhysicalConnectionError(
+ _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+ 'Reserved bits must be unset')
+
+ control_block.accepted = (first_byte >> 4) & 1
+ control_block.encoding = first_byte & 0x3
+ try:
+ control_block.channel_id = self.read_channel_id()
+ except ValueError, e:
+ raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK)
+ control_block.encoded_handshake = self._read_size_and_contents()
+ return control_block
+
+ def _read_flow_control(self, first_byte, control_block):
+ reserved = first_byte & 0x1f
+ if reserved != 0:
+ raise PhysicalConnectionError(
+ _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+ 'Reserved bits must be unset')
+
+ try:
+ control_block.channel_id = self.read_channel_id()
+ control_block.send_quota = self._read_number()
+ except ValueError, e:
+ raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+ str(e))
+
+ return control_block
+
+ def _read_drop_channel(self, first_byte, control_block):
+ reserved = first_byte & 0x1f
+ if reserved != 0:
+ raise PhysicalConnectionError(
+ _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+ 'Reserved bits must be unset')
+
+ try:
+ control_block.channel_id = self.read_channel_id()
+ except ValueError, e:
+ raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK)
+ reason = self._read_size_and_contents()
+ if len(reason) == 0:
+ control_block.drop_code = None
+ control_block.drop_message = ''
+ elif len(reason) >= 2:
+ control_block.drop_code = struct.unpack('!H', reason[:2])[0]
+ control_block.drop_message = reason[2:]
+ else:
+ raise PhysicalConnectionError(
+ _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+ 'Received DropChannel that conains only 1-byte reason')
+ return control_block
+
+ def _read_new_channel_slot(self, first_byte, control_block):
+ reserved = first_byte & 0x1e
+ if reserved != 0:
+ raise PhysicalConnectionError(
+ _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+ 'Reserved bits must be unset')
+ control_block.fallback = first_byte & 1
+ try:
+ control_block.slots = self._read_number()
+ control_block.send_quota = self._read_number()
+ except ValueError, e:
+ raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+ str(e))
+ return control_block
+
+ def read_control_blocks(self):
+ """Reads control block(s).
+
+ Raises:
+ PhysicalConnectionError: when the payload contains invalid control
+ block(s).
+ StopIteration: when no control blocks left.
+ """
+
+ while self._read_position < len(self._data):
+ first_byte = ord(self._data[self._read_position])
+ self._read_position += 1
+ opcode = (first_byte >> 5) & 0x7
+ control_block = _ControlBlock(opcode=opcode)
+ if opcode == _MUX_OPCODE_ADD_CHANNEL_REQUEST:
+ yield self._read_add_channel_request(first_byte, control_block)
+ elif opcode == _MUX_OPCODE_ADD_CHANNEL_RESPONSE:
+ yield self._read_add_channel_response(
+ first_byte, control_block)
+ elif opcode == _MUX_OPCODE_FLOW_CONTROL:
+ yield self._read_flow_control(first_byte, control_block)
+ elif opcode == _MUX_OPCODE_DROP_CHANNEL:
+ yield self._read_drop_channel(first_byte, control_block)
+ elif opcode == _MUX_OPCODE_NEW_CHANNEL_SLOT:
+ yield self._read_new_channel_slot(first_byte, control_block)
+ else:
+ raise PhysicalConnectionError(
+ _DROP_CODE_UNKNOWN_MUX_OPCODE,
+ 'Invalid opcode %d' % opcode)
+
+ assert self._read_position == len(self._data)
+ raise StopIteration
+
+ def remaining_data(self):
+ """Returns remaining data."""
+
+ return self._data[self._read_position:]
+
+
+class _LogicalRequest(object):
+ """Mimics mod_python request."""
+
+ def __init__(self, channel_id, command, path, protocol, headers,
+ connection):
+ """Constructs an instance.
+
+ Args:
+ channel_id: the channel id of the logical channel.
+ command: HTTP request command.
+ path: HTTP request path.
+ headers: HTTP headers.
+ connection: _LogicalConnection instance.
+ """
+
+ self.channel_id = channel_id
+ self.method = command
+ self.uri = path
+ self.protocol = protocol
+ self.headers_in = headers
+ self.connection = connection
+ self.server_terminated = False
+ self.client_terminated = False
+
+ def is_https(self):
+ """Mimics request.is_https(). Returns False because this method is
+ used only by old protocols (hixie and hybi00).
+ """
+
+ return False
+
+
+class _LogicalConnection(object):
+ """Mimics mod_python mp_conn."""
+
+ # For details, see the comment of set_read_state().
+ STATE_ACTIVE = 1
+ STATE_GRACEFULLY_CLOSED = 2
+ STATE_TERMINATED = 3
+
+ def __init__(self, mux_handler, channel_id):
+ """Constructs an instance.
+
+ Args:
+ mux_handler: _MuxHandler instance.
+ channel_id: channel id of this connection.
+ """
+
+ self._mux_handler = mux_handler
+ self._channel_id = channel_id
+ self._incoming_data = ''
+
+ # - Protects _waiting_write_completion
+ # - Signals the thread waiting for completion of write by mux handler
+ self._write_condition = threading.Condition()
+ self._waiting_write_completion = False
+
+ self._read_condition = threading.Condition()
+ self._read_state = self.STATE_ACTIVE
+
+ def get_local_addr(self):
+ """Getter to mimic mp_conn.local_addr."""
+
+ return self._mux_handler.physical_connection.get_local_addr()
+ local_addr = property(get_local_addr)
+
+ def get_remote_addr(self):
+ """Getter to mimic mp_conn.remote_addr."""
+
+ return self._mux_handler.physical_connection.get_remote_addr()
+ remote_addr = property(get_remote_addr)
+
+ def get_memorized_lines(self):
+ """Gets memorized lines. Not supported."""
+
+ raise MuxUnexpectedException('_LogicalConnection does not support '
+ 'get_memorized_lines')
+
+ def write(self, data):
+ """Writes data. mux_handler sends data asynchronously. The caller will
+ be suspended until write done.
+
+ Args:
+ data: data to be written.
+
+ Raises:
+ MuxUnexpectedException: when called before finishing the previous
+ write.
+ """
+
+ try:
+ self._write_condition.acquire()
+ if self._waiting_write_completion:
+ raise MuxUnexpectedException(
+ 'Logical connection %d is already waiting the completion '
+ 'of write' % self._channel_id)
+
+ self._waiting_write_completion = True
+ self._mux_handler.send_data(self._channel_id, data)
+ self._write_condition.wait()
+ # TODO(tyoshino): Raise an exception if woke up by on_writer_done.
+ finally:
+ self._write_condition.release()
+
+ def write_control_data(self, data):
+ """Writes data via the control channel. Don't wait finishing write
+ because this method can be called by mux dispatcher.
+
+ Args:
+ data: data to be written.
+ """
+
+ self._mux_handler.send_control_data(data)
+
+ def on_write_data_done(self):
+ """Called when sending data is completed."""
+
+ try:
+ self._write_condition.acquire()
+ if not self._waiting_write_completion:
+ raise MuxUnexpectedException(
+ 'Invalid call of on_write_data_done for logical '
+ 'connection %d' % self._channel_id)
+ self._waiting_write_completion = False
+ self._write_condition.notify()
+ finally:
+ self._write_condition.release()
+
+ def on_writer_done(self):
+ """Called by the mux handler when the writer thread has finished."""
+
+ try:
+ self._write_condition.acquire()
+ self._waiting_write_completion = False
+ self._write_condition.notify()
+ finally:
+ self._write_condition.release()
+
+
+ def append_frame_data(self, frame_data):
+ """Appends incoming frame data. Called when mux_handler dispatches
+ frame data to the corresponding application.
+
+ Args:
+ frame_data: incoming frame data.
+ """
+
+ self._read_condition.acquire()
+ self._incoming_data += frame_data
+ self._read_condition.notify()
+ self._read_condition.release()
+
+ def read(self, length):
+ """Reads data. Blocks until enough data has arrived via physical
+ connection.
+
+ Args:
+ length: length of data to be read.
+ Raises:
+ LogicalConnectionClosedException: when closing handshake for this
+ logical channel has been received.
+ ConnectionTerminatedException: when the physical connection has
+ closed, or an error is caused on the reader thread.
+ """
+
+ self._read_condition.acquire()
+ while (self._read_state == self.STATE_ACTIVE and
+ len(self._incoming_data) < length):
+ self._read_condition.wait()
+
+ try:
+ if self._read_state == self.STATE_GRACEFULLY_CLOSED:
+ raise LogicalConnectionClosedException(
+ 'Logical channel %d has closed.' % self._channel_id)
+ elif self._read_state == self.STATE_TERMINATED:
+ raise ConnectionTerminatedException(
+ 'Receiving %d byte failed. Logical channel (%d) closed' %
+ (length, self._channel_id))
+
+ value = self._incoming_data[:length]
+ self._incoming_data = self._incoming_data[length:]
+ finally:
+ self._read_condition.release()
+
+ return value
+
+ def set_read_state(self, new_state):
+ """Sets the state of this connection. Called when an event for this
+ connection has occurred.
+
+ Args:
+ new_state: state to be set. new_state must be one of followings:
+ - STATE_GRACEFULLY_CLOSED: when closing handshake for this
+ connection has been received.
+ - STATE_TERMINATED: when the physical connection has closed or
+ DropChannel of this connection has received.
+ """
+
+ self._read_condition.acquire()
+ self._read_state = new_state
+ self._read_condition.notify()
+ self._read_condition.release()
+
+
+class _InnerMessage(object):
+ """Holds the result of _InnerMessageBuilder.build().
+ """
+
+ def __init__(self, opcode, payload):
+ self.opcode = opcode
+ self.payload = payload
+
+
+class _InnerMessageBuilder(object):
+ """A class that holds the context of inner message fragmentation and
+ builds a message from fragmented inner frame(s).
+ """
+
+ def __init__(self):
+ self._control_opcode = None
+ self._pending_control_fragments = []
+ self._message_opcode = None
+ self._pending_message_fragments = []
+ self._frame_handler = self._handle_first
+
+ def _handle_first(self, frame):
+ if frame.opcode == common.OPCODE_CONTINUATION:
+ raise InvalidFrameException('Sending invalid continuation opcode')
+
+ if common.is_control_opcode(frame.opcode):
+ return self._process_first_fragmented_control(frame)
+ else:
+ return self._process_first_fragmented_message(frame)
+
+ def _process_first_fragmented_control(self, frame):
+ self._control_opcode = frame.opcode
+ self._pending_control_fragments.append(frame.payload)
+ if not frame.fin:
+ self._frame_handler = self._handle_fragmented_control
+ return None
+ return self._reassemble_fragmented_control()
+
+ def _process_first_fragmented_message(self, frame):
+ self._message_opcode = frame.opcode
+ self._pending_message_fragments.append(frame.payload)
+ if not frame.fin:
+ self._frame_handler = self._handle_fragmented_message
+ return None
+ return self._reassemble_fragmented_message()
+
+ def _handle_fragmented_control(self, frame):
+ if frame.opcode != common.OPCODE_CONTINUATION:
+ raise InvalidFrameException(
+ 'Sending invalid opcode %d while sending fragmented control '
+ 'message' % frame.opcode)
+ self._pending_control_fragments.append(frame.payload)
+ if not frame.fin:
+ return None
+ return self._reassemble_fragmented_control()
+
+ def _reassemble_fragmented_control(self):
+ opcode = self._control_opcode
+ payload = ''.join(self._pending_control_fragments)
+ self._control_opcode = None
+ self._pending_control_fragments = []
+ if self._message_opcode is not None:
+ self._frame_handler = self._handle_fragmented_message
+ else:
+ self._frame_handler = self._handle_first
+ return _InnerMessage(opcode, payload)
+
+ def _handle_fragmented_message(self, frame):
+ # Sender can interleave a control message while sending fragmented
+ # messages.
+ if common.is_control_opcode(frame.opcode):
+ if self._control_opcode is not None:
+ raise MuxUnexpectedException(
+ 'Should not reach here(Bug in builder)')
+ return self._process_first_fragmented_control(frame)
+
+ if frame.opcode != common.OPCODE_CONTINUATION:
+ raise InvalidFrameException(
+ 'Sending invalid opcode %d while sending fragmented message' %
+ frame.opcode)
+ self._pending_message_fragments.append(frame.payload)
+ if not frame.fin:
+ return None
+ return self._reassemble_fragmented_message()
+
+ def _reassemble_fragmented_message(self):
+ opcode = self._message_opcode
+ payload = ''.join(self._pending_message_fragments)
+ self._message_opcode = None
+ self._pending_message_fragments = []
+ self._frame_handler = self._handle_first
+ return _InnerMessage(opcode, payload)
+
+ def build(self, frame):
+ """Build an inner message. Returns an _InnerMessage instance when
+ the given frame is the last fragmented frame. Returns None otherwise.
+
+ Args:
+ frame: an inner frame.
+ Raises:
+ InvalidFrameException: when received invalid opcode. (e.g.
+ receiving non continuation data opcode but the fin flag of
+ the previous inner frame was not set.)
+ """
+
+ return self._frame_handler(frame)
+
+
+class _LogicalStream(Stream):
+ """Mimics the Stream class. This class interprets multiplexed WebSocket
+ frames.
+ """
+
+ def __init__(self, request, stream_options, send_quota, receive_quota):
+ """Constructs an instance.
+
+ Args:
+ request: _LogicalRequest instance.
+ stream_options: StreamOptions instance.
+ send_quota: Initial send quota.
+ receive_quota: Initial receive quota.
+ """
+
+ # Physical stream is responsible for masking.
+ stream_options.unmask_receive = False
+ Stream.__init__(self, request, stream_options)
+
+ self._send_closed = False
+ self._send_quota = send_quota
+ # - Protects _send_closed and _send_quota
+ # - Signals the thread waiting for send quota replenished
+ self._send_condition = threading.Condition()
+
+ # The opcode of the first frame in messages.
+ self._message_opcode = common.OPCODE_TEXT
+ # True when the last message was fragmented.
+ self._last_message_was_fragmented = False
+
+ self._receive_quota = receive_quota
+ self._write_inner_frame_semaphore = threading.Semaphore()
+
+ self._inner_message_builder = _InnerMessageBuilder()
+
+ def _create_inner_frame(self, opcode, payload, end=True):
+ frame = Frame(fin=end, opcode=opcode, payload=payload)
+ for frame_filter in self._options.outgoing_frame_filters:
+ frame_filter.filter(frame)
+
+ if len(payload) != len(frame.payload):
+ raise MuxUnexpectedException(
+ 'Mux extension must not be used after extensions which change '
+ ' frame boundary')
+
+ first_byte = ((frame.fin << 7) | (frame.rsv1 << 6) |
+ (frame.rsv2 << 5) | (frame.rsv3 << 4) | frame.opcode)
+ return chr(first_byte) + frame.payload
+
+ def _write_inner_frame(self, opcode, payload, end=True):
+ payload_length = len(payload)
+ write_position = 0
+
+ try:
+ # An inner frame will be fragmented if there is no enough send
+ # quota. This semaphore ensures that fragmented inner frames are
+ # sent in order on the logical channel.
+ # Note that frames that come from other logical channels or
+ # multiplexing control blocks can be inserted between fragmented
+ # inner frames on the physical channel.
+ self._write_inner_frame_semaphore.acquire()
+
+ # Consume an octet quota when this is the first fragmented frame.
+ if opcode != common.OPCODE_CONTINUATION:
+ try:
+ self._send_condition.acquire()
+ while (not self._send_closed) and self._send_quota == 0:
+ self._send_condition.wait()
+
+ if self._send_closed:
+ raise BadOperationException(
+ 'Logical connection %d is closed' %
+ self._request.channel_id)
+
+ self._send_quota -= 1
+ finally:
+ self._send_condition.release()
+
+ while write_position < payload_length:
+ try:
+ self._send_condition.acquire()
+ while (not self._send_closed) and self._send_quota == 0:
+ self._logger.debug(
+ 'No quota. Waiting FlowControl message for %d.' %
+ self._request.channel_id)
+ self._send_condition.wait()
+
+ if self._send_closed:
+ raise BadOperationException(
+ 'Logical connection %d is closed' %
+ self.request._channel_id)
+
+ remaining = payload_length - write_position
+ write_length = min(self._send_quota, remaining)
+ inner_frame_end = (
+ end and
+ (write_position + write_length == payload_length))
+
+ inner_frame = self._create_inner_frame(
+ opcode,
+ payload[write_position:write_position+write_length],
+ inner_frame_end)
+ self._send_quota -= write_length
+ self._logger.debug('Consumed quota=%d, remaining=%d' %
+ (write_length, self._send_quota))
+ finally:
+ self._send_condition.release()
+
+ # Writing data will block the worker so we need to release
+ # _send_condition before writing.
+ self._logger.debug('Sending inner frame: %r' % inner_frame)
+ self._request.connection.write(inner_frame)
+ write_position += write_length
+
+ opcode = common.OPCODE_CONTINUATION
+
+ except ValueError, e:
+ raise BadOperationException(e)
+ finally:
+ self._write_inner_frame_semaphore.release()
+
+ def replenish_send_quota(self, send_quota):
+ """Replenish send quota."""
+
+ try:
+ self._send_condition.acquire()
+ if self._send_quota + send_quota > 0x7FFFFFFFFFFFFFFF:
+ self._send_quota = 0
+ raise LogicalChannelError(
+ self._request.channel_id, _DROP_CODE_SEND_QUOTA_OVERFLOW)
+ self._send_quota += send_quota
+ self._logger.debug('Replenished send quota for channel id %d: %d' %
+ (self._request.channel_id, self._send_quota))
+ finally:
+ self._send_condition.notify()
+ self._send_condition.release()
+
+ def consume_receive_quota(self, amount):
+ """Consumes receive quota. Returns False on failure."""
+
+ if self._receive_quota < amount:
+ self._logger.debug('Violate quota on channel id %d: %d < %d' %
+ (self._request.channel_id,
+ self._receive_quota, amount))
+ return False
+ self._receive_quota -= amount
+ return True
+
+ def send_message(self, message, end=True, binary=False):
+ """Override Stream.send_message."""
+
+ if self._request.server_terminated:
+ raise BadOperationException(
+ 'Requested send_message after sending out a closing handshake')
+
+ if binary and isinstance(message, unicode):
+ raise BadOperationException(
+ 'Message for binary frame must be instance of str')
+
+ if binary:
+ opcode = common.OPCODE_BINARY
+ else:
+ opcode = common.OPCODE_TEXT
+ message = message.encode('utf-8')
+
+ for message_filter in self._options.outgoing_message_filters:
+ message = message_filter.filter(message, end, binary)
+
+ if self._last_message_was_fragmented:
+ if opcode != self._message_opcode:
+ raise BadOperationException('Message types are different in '
+ 'frames for the same message')
+ opcode = common.OPCODE_CONTINUATION
+ else:
+ self._message_opcode = opcode
+
+ self._write_inner_frame(opcode, message, end)
+ self._last_message_was_fragmented = not end
+
+ def _receive_frame(self):
+ """Overrides Stream._receive_frame.
+
+ In addition to call Stream._receive_frame, this method adds the amount
+ of payload to receiving quota and sends FlowControl to the client.
+ We need to do it here because Stream.receive_message() handles
+ control frames internally.
+ """
+
+ opcode, payload, fin, rsv1, rsv2, rsv3 = Stream._receive_frame(self)
+ amount = len(payload)
+ # Replenish extra one octet when receiving the first fragmented frame.
+ if opcode != common.OPCODE_CONTINUATION:
+ amount += 1
+ self._receive_quota += amount
+ frame_data = _create_flow_control(self._request.channel_id,
+ amount)
+ self._logger.debug('Sending flow control for %d, replenished=%d' %
+ (self._request.channel_id, amount))
+ self._request.connection.write_control_data(frame_data)
+ return opcode, payload, fin, rsv1, rsv2, rsv3
+
+ def _get_message_from_frame(self, frame):
+ """Overrides Stream._get_message_from_frame.
+ """
+
+ try:
+ inner_message = self._inner_message_builder.build(frame)
+ except InvalidFrameException:
+ raise LogicalChannelError(
+ self._request.channel_id, _DROP_CODE_BAD_FRAGMENTATION)
+
+ if inner_message is None:
+ return None
+ self._original_opcode = inner_message.opcode
+ return inner_message.payload
+
+ def receive_message(self):
+ """Overrides Stream.receive_message."""
+
+ # Just call Stream.receive_message(), but catch
+ # LogicalConnectionClosedException, which is raised when the logical
+ # connection has closed gracefully.
+ try:
+ return Stream.receive_message(self)
+ except LogicalConnectionClosedException, e:
+ self._logger.debug('%s', e)
+ return None
+
+ def _send_closing_handshake(self, code, reason):
+ """Overrides Stream._send_closing_handshake."""
+
+ body = create_closing_handshake_body(code, reason)
+ self._logger.debug('Sending closing handshake for %d: (%r, %r)' %
+ (self._request.channel_id, code, reason))
+ self._write_inner_frame(common.OPCODE_CLOSE, body, end=True)
+
+ self._request.server_terminated = True
+
+ def send_ping(self, body=''):
+ """Overrides Stream.send_ping"""
+
+ self._logger.debug('Sending ping on logical channel %d: %r' %
+ (self._request.channel_id, body))
+ self._write_inner_frame(common.OPCODE_PING, body, end=True)
+
+ self._ping_queue.append(body)
+
+ def _send_pong(self, body):
+ """Overrides Stream._send_pong"""
+
+ self._logger.debug('Sending pong on logical channel %d: %r' %
+ (self._request.channel_id, body))
+ self._write_inner_frame(common.OPCODE_PONG, body, end=True)
+
+ def close_connection(self, code=common.STATUS_NORMAL_CLOSURE, reason=''):
+ """Overrides Stream.close_connection."""
+
+ # TODO(bashi): Implement
+ self._logger.debug('Closing logical connection %d' %
+ self._request.channel_id)
+ self._request.server_terminated = True
+
+ def stop_sending(self):
+ """Stops accepting new send operation (_write_inner_frame)."""
+
+ self._send_condition.acquire()
+ self._send_closed = True
+ self._send_condition.notify()
+ self._send_condition.release()
+
+
+class _OutgoingData(object):
+ """A structure that holds data to be sent via physical connection and
+ origin of the data.
+ """
+
+ def __init__(self, channel_id, data):
+ self.channel_id = channel_id
+ self.data = data
+
+
+class _PhysicalConnectionWriter(threading.Thread):
+ """A thread that is responsible for writing data to physical connection.
+
+ TODO(bashi): Make sure there is no thread-safety problem when the reader
+ thread reads data from the same socket at a time.
+ """
+
+ def __init__(self, mux_handler):
+ """Constructs an instance.
+
+ Args:
+ mux_handler: _MuxHandler instance.
+ """
+
+ threading.Thread.__init__(self)
+ self._logger = util.get_class_logger(self)
+ self._mux_handler = mux_handler
+ self.setDaemon(True)
+
+ # When set, make this thread stop accepting new data, flush pending
+ # data and exit.
+ self._stop_requested = False
+ # The close code of the physical connection.
+ self._close_code = common.STATUS_NORMAL_CLOSURE
+ # Deque for passing write data. It's protected by _deque_condition
+ # until _stop_requested is set.
+ self._deque = collections.deque()
+ # - Protects _deque, _stop_requested and _close_code
+ # - Signals threads waiting for them to be available
+ self._deque_condition = threading.Condition()
+
+ def put_outgoing_data(self, data):
+ """Puts outgoing data.
+
+ Args:
+ data: _OutgoingData instance.
+
+ Raises:
+ BadOperationException: when the thread has been requested to
+ terminate.
+ """
+
+ try:
+ self._deque_condition.acquire()
+ if self._stop_requested:
+ raise BadOperationException('Cannot write data anymore')
+
+ self._deque.append(data)
+ self._deque_condition.notify()
+ finally:
+ self._deque_condition.release()
+
+ def _write_data(self, outgoing_data):
+ message = (_encode_channel_id(outgoing_data.channel_id) +
+ outgoing_data.data)
+ try:
+ self._mux_handler.physical_stream.send_message(
+ message=message, end=True, binary=True)
+ except Exception, e:
+ util.prepend_message_to_exception(
+ 'Failed to send message to %r: ' %
+ (self._mux_handler.physical_connection.remote_addr,), e)
+ raise
+
+ # TODO(bashi): It would be better to block the thread that sends
+ # control data as well.
+ if outgoing_data.channel_id != _CONTROL_CHANNEL_ID:
+ self._mux_handler.notify_write_data_done(outgoing_data.channel_id)
+
+ def run(self):
+ try:
+ self._deque_condition.acquire()
+ while not self._stop_requested:
+ if len(self._deque) == 0:
+ self._deque_condition.wait()
+ continue
+
+ outgoing_data = self._deque.popleft()
+
+ self._deque_condition.release()
+ self._write_data(outgoing_data)
+ self._deque_condition.acquire()
+
+ # Flush deque.
+ #
+ # At this point, self._deque_condition is always acquired.
+ try:
+ while len(self._deque) > 0:
+ outgoing_data = self._deque.popleft()
+ self._write_data(outgoing_data)
+ finally:
+ self._deque_condition.release()
+
+ # Close physical connection.
+ try:
+ # Don't wait the response here. The response will be read
+ # by the reader thread.
+ self._mux_handler.physical_stream.close_connection(
+ self._close_code, wait_response=False)
+ except Exception, e:
+ util.prepend_message_to_exception(
+ 'Failed to close the physical connection: %r' % e)
+ raise
+ finally:
+ self._mux_handler.notify_writer_done()
+
+ def stop(self, close_code=common.STATUS_NORMAL_CLOSURE):
+ """Stops the writer thread."""
+
+ self._deque_condition.acquire()
+ self._stop_requested = True
+ self._close_code = close_code
+ self._deque_condition.notify()
+ self._deque_condition.release()
+
+
+class _PhysicalConnectionReader(threading.Thread):
+ """A thread that is responsible for reading data from physical connection.
+ """
+
+ def __init__(self, mux_handler):
+ """Constructs an instance.
+
+ Args:
+ mux_handler: _MuxHandler instance.
+ """
+
+ threading.Thread.__init__(self)
+ self._logger = util.get_class_logger(self)
+ self._mux_handler = mux_handler
+ self.setDaemon(True)
+
+ def run(self):
+ while True:
+ try:
+ physical_stream = self._mux_handler.physical_stream
+ message = physical_stream.receive_message()
+ if message is None:
+ break
+ # Below happens only when a data message is received.
+ opcode = physical_stream.get_last_received_opcode()
+ if opcode != common.OPCODE_BINARY:
+ self._mux_handler.fail_physical_connection(
+ _DROP_CODE_INVALID_ENCAPSULATING_MESSAGE,
+ 'Received a text message on physical connection')
+ break
+
+ except ConnectionTerminatedException, e:
+ self._logger.debug('%s', e)
+ break
+
+ try:
+ self._mux_handler.dispatch_message(message)
+ except PhysicalConnectionError, e:
+ self._mux_handler.fail_physical_connection(
+ e.drop_code, e.message)
+ break
+ except LogicalChannelError, e:
+ self._mux_handler.fail_logical_channel(
+ e.channel_id, e.drop_code, e.message)
+ except Exception, e:
+ self._logger.debug(traceback.format_exc())
+ break
+
+ self._mux_handler.notify_reader_done()
+
+
+class _Worker(threading.Thread):
+ """A thread that is responsible for running the corresponding application
+ handler.
+ """
+
+ def __init__(self, mux_handler, request):
+ """Constructs an instance.
+
+ Args:
+ mux_handler: _MuxHandler instance.
+ request: _LogicalRequest instance.
+ """
+
+ threading.Thread.__init__(self)
+ self._logger = util.get_class_logger(self)
+ self._mux_handler = mux_handler
+ self._request = request
+ self.setDaemon(True)
+
+ def run(self):
+ self._logger.debug('Logical channel worker started. (id=%d)' %
+ self._request.channel_id)
+ try:
+ # Non-critical exceptions will be handled by dispatcher.
+ self._mux_handler.dispatcher.transfer_data(self._request)
+ except LogicalChannelError, e:
+ self._mux_handler.fail_logical_channel(
+ e.channel_id, e.drop_code, e.message)
+ finally:
+ self._mux_handler.notify_worker_done(self._request.channel_id)
+
+
+class _MuxHandshaker(hybi.Handshaker):
+ """Opening handshake processor for multiplexing."""
+
+ _DUMMY_WEBSOCKET_KEY = 'dGhlIHNhbXBsZSBub25jZQ=='
+
+ def __init__(self, request, dispatcher, send_quota, receive_quota):
+ """Constructs an instance.
+ Args:
+ request: _LogicalRequest instance.
+ dispatcher: Dispatcher instance (dispatch.Dispatcher).
+ send_quota: Initial send quota.
+ receive_quota: Initial receive quota.
+ """
+
+ hybi.Handshaker.__init__(self, request, dispatcher)
+ self._send_quota = send_quota
+ self._receive_quota = receive_quota
+
+ # Append headers which should not be included in handshake field of
+ # AddChannelRequest.
+ # TODO(bashi): Make sure whether we should raise exception when
+ # these headers are included already.
+ request.headers_in[common.UPGRADE_HEADER] = (
+ common.WEBSOCKET_UPGRADE_TYPE)
+ request.headers_in[common.SEC_WEBSOCKET_VERSION_HEADER] = (
+ str(common.VERSION_HYBI_LATEST))
+ request.headers_in[common.SEC_WEBSOCKET_KEY_HEADER] = (
+ self._DUMMY_WEBSOCKET_KEY)
+
+ def _create_stream(self, stream_options):
+ """Override hybi.Handshaker._create_stream."""
+
+ self._logger.debug('Creating logical stream for %d' %
+ self._request.channel_id)
+ return _LogicalStream(
+ self._request, stream_options, self._send_quota,
+ self._receive_quota)
+
+ def _create_handshake_response(self, accept):
+ """Override hybi._create_handshake_response."""
+
+ response = []
+
+ response.append('HTTP/1.1 101 Switching Protocols\r\n')
+
+ # Upgrade and Sec-WebSocket-Accept should be excluded.
+ response.append('%s: %s\r\n' % (
+ common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE))
+ if self._request.ws_protocol is not None:
+ response.append('%s: %s\r\n' % (
+ common.SEC_WEBSOCKET_PROTOCOL_HEADER,
+ self._request.ws_protocol))
+ if (self._request.ws_extensions is not None and
+ len(self._request.ws_extensions) != 0):
+ response.append('%s: %s\r\n' % (
+ common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
+ common.format_extensions(self._request.ws_extensions)))
+ response.append('\r\n')
+
+ return ''.join(response)
+
+ def _send_handshake(self, accept):
+ """Override hybi.Handshaker._send_handshake."""
+
+ # Don't send handshake response for the default channel
+ if self._request.channel_id == _DEFAULT_CHANNEL_ID:
+ return
+
+ handshake_response = self._create_handshake_response(accept)
+ frame_data = _create_add_channel_response(
+ self._request.channel_id,
+ handshake_response)
+ self._logger.debug('Sending handshake response for %d: %r' %
+ (self._request.channel_id, frame_data))
+ self._request.connection.write_control_data(frame_data)
+
+
+class _LogicalChannelData(object):
+ """A structure that holds information about logical channel.
+ """
+
+ def __init__(self, request, worker):
+ self.request = request
+ self.worker = worker
+ self.drop_code = _DROP_CODE_NORMAL_CLOSURE
+ self.drop_message = ''
+
+
+class _HandshakeDeltaBase(object):
+ """A class that holds information for delta-encoded handshake."""
+
+ def __init__(self, headers):
+ self._headers = headers
+
+ def create_headers(self, delta=None):
+ """Creates request headers for an AddChannelRequest that has
+ delta-encoded handshake.
+
+ Args:
+ delta: headers should be overridden.
+ """
+
+ headers = copy.copy(self._headers)
+ if delta:
+ for key, value in delta.items():
+ # The spec requires that a header with an empty value is
+ # removed from the delta base.
+ if len(value) == 0 and headers.has_key(key):
+ del headers[key]
+ else:
+ headers[key] = value
+ return headers
+
+
+class _MuxHandler(object):
+ """Multiplexing handler. When a handler starts, it launches three
+ threads; the reader thread, the writer thread, and a worker thread.
+
+ The reader thread reads data from the physical stream, i.e., the
+ ws_stream object of the underlying websocket connection. The reader
+ thread interprets multiplexed frames and dispatches them to logical
+ channels. Methods of this class are mostly called by the reader thread.
+
+ The writer thread sends multiplexed frames which are created by
+ logical channels via the physical connection.
+
+ The worker thread launched at the starting point handles the
+ "Implicitly Opened Connection". If multiplexing handler receives
+ an AddChannelRequest and accepts it, the handler will launch a new worker
+ thread and dispatch the request to it.
+ """
+
+ def __init__(self, request, dispatcher):
+ """Constructs an instance.
+
+ Args:
+ request: mod_python request of the physical connection.
+ dispatcher: Dispatcher instance (dispatch.Dispatcher).
+ """
+
+ self.original_request = request
+ self.dispatcher = dispatcher
+ self.physical_connection = request.connection
+ self.physical_stream = request.ws_stream
+ self._logger = util.get_class_logger(self)
+ self._logical_channels = {}
+ self._logical_channels_condition = threading.Condition()
+ # Holds client's initial quota
+ self._channel_slots = collections.deque()
+ self._handshake_base = None
+ self._worker_done_notify_received = False
+ self._reader = None
+ self._writer = None
+
+ def start(self):
+ """Starts the handler.
+
+ Raises:
+ MuxUnexpectedException: when the handler already started, or when
+ opening handshake of the default channel fails.
+ """
+
+ if self._reader or self._writer:
+ raise MuxUnexpectedException('MuxHandler already started')
+
+ self._reader = _PhysicalConnectionReader(self)
+ self._writer = _PhysicalConnectionWriter(self)
+ self._reader.start()
+ self._writer.start()
+
+ # Create "Implicitly Opened Connection".
+ logical_connection = _LogicalConnection(self, _DEFAULT_CHANNEL_ID)
+ headers = copy.copy(self.original_request.headers_in)
+ # Add extensions for logical channel.
+ headers[common.SEC_WEBSOCKET_EXTENSIONS_HEADER] = (
+ common.format_extensions(
+ self.original_request.mux_processor.extensions()))
+ self._handshake_base = _HandshakeDeltaBase(headers)
+ logical_request = _LogicalRequest(
+ _DEFAULT_CHANNEL_ID,
+ self.original_request.method,
+ self.original_request.uri,
+ self.original_request.protocol,
+ self._handshake_base.create_headers(),
+ logical_connection)
+ # Client's send quota for the implicitly opened connection is zero,
+ # but we will send FlowControl later so set the initial quota to
+ # _INITIAL_QUOTA_FOR_CLIENT.
+ self._channel_slots.append(_INITIAL_QUOTA_FOR_CLIENT)
+ send_quota = self.original_request.mux_processor.quota()
+ if not self._do_handshake_for_logical_request(
+ logical_request, send_quota=send_quota):
+ raise MuxUnexpectedException(
+ 'Failed handshake on the default channel id')
+ self._add_logical_channel(logical_request)
+
+ # Send FlowControl for the implicitly opened connection.
+ frame_data = _create_flow_control(_DEFAULT_CHANNEL_ID,
+ _INITIAL_QUOTA_FOR_CLIENT)
+ logical_request.connection.write_control_data(frame_data)
+
+ def add_channel_slots(self, slots, send_quota):
+ """Adds channel slots.
+
+ Args:
+ slots: number of slots to be added.
+ send_quota: initial send quota for slots.
+ """
+
+ self._channel_slots.extend([send_quota] * slots)
+ # Send NewChannelSlot to client.
+ frame_data = _create_new_channel_slot(slots, send_quota)
+ self.send_control_data(frame_data)
+
+ def wait_until_done(self, timeout=None):
+ """Waits until all workers are done. Returns False when timeout has
+ occurred. Returns True on success.
+
+ Args:
+ timeout: timeout in sec.
+ """
+
+ self._logical_channels_condition.acquire()
+ try:
+ while len(self._logical_channels) > 0:
+ self._logger.debug('Waiting workers(%d)...' %
+ len(self._logical_channels))
+ self._worker_done_notify_received = False
+ self._logical_channels_condition.wait(timeout)
+ if not self._worker_done_notify_received:
+ self._logger.debug('Waiting worker(s) timed out')
+ return False
+ finally:
+ self._logical_channels_condition.release()
+
+ # Flush pending outgoing data
+ self._writer.stop()
+ self._writer.join()
+
+ return True
+
+ def notify_write_data_done(self, channel_id):
+ """Called by the writer thread when a write operation has done.
+
+ Args:
+ channel_id: objective channel id.
+ """
+
+ try:
+ self._logical_channels_condition.acquire()
+ if channel_id in self._logical_channels:
+ channel_data = self._logical_channels[channel_id]
+ channel_data.request.connection.on_write_data_done()
+ else:
+ self._logger.debug('Seems that logical channel for %d has gone'
+ % channel_id)
+ finally:
+ self._logical_channels_condition.release()
+
+ def send_control_data(self, data):
+ """Sends data via the control channel.
+
+ Args:
+ data: data to be sent.
+ """
+
+ self._writer.put_outgoing_data(_OutgoingData(
+ channel_id=_CONTROL_CHANNEL_ID, data=data))
+
+ def send_data(self, channel_id, data):
+ """Sends data via given logical channel. This method is called by
+ worker threads.
+
+ Args:
+ channel_id: objective channel id.
+ data: data to be sent.
+ """
+
+ self._writer.put_outgoing_data(_OutgoingData(
+ channel_id=channel_id, data=data))
+
+ def _send_drop_channel(self, channel_id, code=None, message=''):
+ frame_data = _create_drop_channel(channel_id, code, message)
+ self._logger.debug(
+ 'Sending drop channel for channel id %d' % channel_id)
+ self.send_control_data(frame_data)
+
+ def _send_error_add_channel_response(self, channel_id, status=None):
+ if status is None:
+ status = common.HTTP_STATUS_BAD_REQUEST
+
+ if status in _HTTP_BAD_RESPONSE_MESSAGES:
+ message = _HTTP_BAD_RESPONSE_MESSAGES[status]
+ else:
+ self._logger.debug('Response message for %d is not found' % status)
+ message = '???'
+
+ response = 'HTTP/1.1 %d %s\r\n\r\n' % (status, message)
+ frame_data = _create_add_channel_response(channel_id,
+ encoded_handshake=response,
+ encoding=0, rejected=True)
+ self.send_control_data(frame_data)
+
+ def _create_logical_request(self, block):
+ if block.channel_id == _CONTROL_CHANNEL_ID:
+ # TODO(bashi): Raise PhysicalConnectionError with code 2006
+ # instead of MuxUnexpectedException.
+ raise MuxUnexpectedException(
+ 'Received the control channel id (0) as objective channel '
+ 'id for AddChannel')
+
+ if block.encoding > _HANDSHAKE_ENCODING_DELTA:
+ raise PhysicalConnectionError(
+ _DROP_CODE_UNKNOWN_REQUEST_ENCODING)
+
+ method, path, version, headers = _parse_request_text(
+ block.encoded_handshake)
+ if block.encoding == _HANDSHAKE_ENCODING_DELTA:
+ headers = self._handshake_base.create_headers(headers)
+
+ connection = _LogicalConnection(self, block.channel_id)
+ request = _LogicalRequest(block.channel_id, method, path, version,
+ headers, connection)
+ return request
+
+ def _do_handshake_for_logical_request(self, request, send_quota=0):
+ try:
+ receive_quota = self._channel_slots.popleft()
+ except IndexError:
+ raise LogicalChannelError(
+ request.channel_id, _DROP_CODE_NEW_CHANNEL_SLOT_VIOLATION)
+
+ handshaker = _MuxHandshaker(request, self.dispatcher,
+ send_quota, receive_quota)
+ try:
+ handshaker.do_handshake()
+ except handshake.VersionException, e:
+ self._logger.info('%s', e)
+ self._send_error_add_channel_response(
+ request.channel_id, status=common.HTTP_STATUS_BAD_REQUEST)
+ return False
+ except handshake.HandshakeException, e:
+ # TODO(bashi): Should we _Fail the Logical Channel_ with 3001
+ # instead?
+ self._logger.info('%s', e)
+ self._send_error_add_channel_response(request.channel_id,
+ status=e.status)
+ return False
+ except handshake.AbortedByUserException, e:
+ self._logger.info('%s', e)
+ self._send_error_add_channel_response(request.channel_id)
+ return False
+
+ return True
+
+ def _add_logical_channel(self, logical_request):
+ try:
+ self._logical_channels_condition.acquire()
+ if logical_request.channel_id in self._logical_channels:
+ self._logger.debug('Channel id %d already exists' %
+ logical_request.channel_id)
+ raise PhysicalConnectionError(
+ _DROP_CODE_CHANNEL_ALREADY_EXISTS,
+ 'Channel id %d already exists' %
+ logical_request.channel_id)
+ worker = _Worker(self, logical_request)
+ channel_data = _LogicalChannelData(logical_request, worker)
+ self._logical_channels[logical_request.channel_id] = channel_data
+ worker.start()
+ finally:
+ self._logical_channels_condition.release()
+
+ def _process_add_channel_request(self, block):
+ try:
+ logical_request = self._create_logical_request(block)
+ except ValueError, e:
+ self._logger.debug('Failed to create logical request: %r' % e)
+ self._send_error_add_channel_response(
+ block.channel_id, status=common.HTTP_STATUS_BAD_REQUEST)
+ return
+ if self._do_handshake_for_logical_request(logical_request):
+ if block.encoding == _HANDSHAKE_ENCODING_IDENTITY:
+ # Update handshake base.
+ # TODO(bashi): Make sure this is the right place to update
+ # handshake base.
+ self._handshake_base = _HandshakeDeltaBase(
+ logical_request.headers_in)
+ self._add_logical_channel(logical_request)
+ else:
+ self._send_error_add_channel_response(
+ block.channel_id, status=common.HTTP_STATUS_BAD_REQUEST)
+
+ def _process_flow_control(self, block):
+ try:
+ self._logical_channels_condition.acquire()
+ if not block.channel_id in self._logical_channels:
+ return
+ channel_data = self._logical_channels[block.channel_id]
+ channel_data.request.ws_stream.replenish_send_quota(
+ block.send_quota)
+ finally:
+ self._logical_channels_condition.release()
+
+ def _process_drop_channel(self, block):
+ self._logger.debug(
+ 'DropChannel received for %d: code=%r, reason=%r' %
+ (block.channel_id, block.drop_code, block.drop_message))
+ try:
+ self._logical_channels_condition.acquire()
+ if not block.channel_id in self._logical_channels:
+ return
+ channel_data = self._logical_channels[block.channel_id]
+ channel_data.drop_code = _DROP_CODE_ACKNOWLEDGED
+
+ # Close the logical channel
+ channel_data.request.connection.set_read_state(
+ _LogicalConnection.STATE_TERMINATED)
+ channel_data.request.ws_stream.stop_sending()
+ finally:
+ self._logical_channels_condition.release()
+
+ def _process_control_blocks(self, parser):
+ for control_block in parser.read_control_blocks():
+ opcode = control_block.opcode
+ self._logger.debug('control block received, opcode: %d' % opcode)
+ if opcode == _MUX_OPCODE_ADD_CHANNEL_REQUEST:
+ self._process_add_channel_request(control_block)
+ elif opcode == _MUX_OPCODE_ADD_CHANNEL_RESPONSE:
+ raise PhysicalConnectionError(
+ _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+ 'Received AddChannelResponse')
+ elif opcode == _MUX_OPCODE_FLOW_CONTROL:
+ self._process_flow_control(control_block)
+ elif opcode == _MUX_OPCODE_DROP_CHANNEL:
+ self._process_drop_channel(control_block)
+ elif opcode == _MUX_OPCODE_NEW_CHANNEL_SLOT:
+ raise PhysicalConnectionError(
+ _DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+ 'Received NewChannelSlot')
+ else:
+ raise MuxUnexpectedException(
+ 'Unexpected opcode %r' % opcode)
+
+ def _process_logical_frame(self, channel_id, parser):
+ self._logger.debug('Received a frame. channel id=%d' % channel_id)
+ try:
+ self._logical_channels_condition.acquire()
+ if not channel_id in self._logical_channels:
+ # We must ignore the message for an inactive channel.
+ return
+ channel_data = self._logical_channels[channel_id]
+ fin, rsv1, rsv2, rsv3, opcode, payload = parser.read_inner_frame()
+ consuming_byte = len(payload)
+ if opcode != common.OPCODE_CONTINUATION:
+ consuming_byte += 1
+ if not channel_data.request.ws_stream.consume_receive_quota(
+ consuming_byte):
+ # The client violates quota. Close logical channel.
+ raise LogicalChannelError(
+ channel_id, _DROP_CODE_SEND_QUOTA_VIOLATION)
+ header = create_header(opcode, len(payload), fin, rsv1, rsv2, rsv3,
+ mask=False)
+ frame_data = header + payload
+ channel_data.request.connection.append_frame_data(frame_data)
+ finally:
+ self._logical_channels_condition.release()
+
+ def dispatch_message(self, message):
+ """Dispatches message. The reader thread calls this method.
+
+ Args:
+ message: a message that contains encapsulated frame.
+ Raises:
+ PhysicalConnectionError: if the message contains physical
+ connection level errors.
+ LogicalChannelError: if the message contains logical channel
+ level errors.
+ """
+
+ parser = _MuxFramePayloadParser(message)
+ try:
+ channel_id = parser.read_channel_id()
+ except ValueError, e:
+ raise PhysicalConnectionError(_DROP_CODE_CHANNEL_ID_TRUNCATED)
+ if channel_id == _CONTROL_CHANNEL_ID:
+ self._process_control_blocks(parser)
+ else:
+ self._process_logical_frame(channel_id, parser)
+
+ def notify_worker_done(self, channel_id):
+ """Called when a worker has finished.
+
+ Args:
+ channel_id: channel id corresponded with the worker.
+ """
+
+ self._logger.debug('Worker for channel id %d terminated' % channel_id)
+ try:
+ self._logical_channels_condition.acquire()
+ if not channel_id in self._logical_channels:
+ raise MuxUnexpectedException(
+ 'Channel id %d not found' % channel_id)
+ channel_data = self._logical_channels.pop(channel_id)
+ finally:
+ self._worker_done_notify_received = True
+ self._logical_channels_condition.notify()
+ self._logical_channels_condition.release()
+
+ if not channel_data.request.server_terminated:
+ self._send_drop_channel(
+ channel_id, code=channel_data.drop_code,
+ message=channel_data.drop_message)
+
+ def notify_reader_done(self):
+ """This method is called by the reader thread when the reader has
+ finished.
+ """
+
+ self._logger.debug(
+ 'Termiating all logical connections waiting for incoming data '
+ '...')
+ self._logical_channels_condition.acquire()
+ for channel_data in self._logical_channels.values():
+ try:
+ channel_data.request.connection.set_read_state(
+ _LogicalConnection.STATE_TERMINATED)
+ except Exception:
+ self._logger.debug(traceback.format_exc())
+ self._logical_channels_condition.release()
+
+ def notify_writer_done(self):
+ """This method is called by the writer thread when the writer has
+ finished.
+ """
+
+ self._logger.debug(
+ 'Termiating all logical connections waiting for write '
+ 'completion ...')
+ self._logical_channels_condition.acquire()
+ for channel_data in self._logical_channels.values():
+ try:
+ channel_data.request.connection.on_writer_done()
+ except Exception:
+ self._logger.debug(traceback.format_exc())
+ self._logical_channels_condition.release()
+
+ def fail_physical_connection(self, code, message):
+ """Fail the physical connection.
+
+ Args:
+ code: drop reason code.
+ message: drop message.
+ """
+
+ self._logger.debug('Failing the physical connection...')
+ self._send_drop_channel(_CONTROL_CHANNEL_ID, code, message)
+ self._writer.stop(common.STATUS_INTERNAL_ENDPOINT_ERROR)
+
+ def fail_logical_channel(self, channel_id, code, message):
+ """Fail a logical channel.
+
+ Args:
+ channel_id: channel id.
+ code: drop reason code.
+ message: drop message.
+ """
+
+ self._logger.debug('Failing logical channel %d...' % channel_id)
+ try:
+ self._logical_channels_condition.acquire()
+ if channel_id in self._logical_channels:
+ channel_data = self._logical_channels[channel_id]
+ # Close the logical channel. notify_worker_done() will be
+ # called later and it will send DropChannel.
+ channel_data.drop_code = code
+ channel_data.drop_message = message
+
+ channel_data.request.connection.set_read_state(
+ _LogicalConnection.STATE_TERMINATED)
+ channel_data.request.ws_stream.stop_sending()
+ else:
+ self._send_drop_channel(channel_id, code, message)
+ finally:
+ self._logical_channels_condition.release()
+
+
+def use_mux(request):
+ return hasattr(request, 'mux_processor') and (
+ request.mux_processor.is_active())
+
+
+def start(request, dispatcher):
+ mux_handler = _MuxHandler(request, dispatcher)
+ mux_handler.start()
+
+ mux_handler.add_channel_slots(_INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ _INITIAL_QUOTA_FOR_CLIENT)
+
+ mux_handler.wait_until_done()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/standalone.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/standalone.py
new file mode 100755
index 000000000..24c299eaf
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/standalone.py
@@ -0,0 +1,1193 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Standalone WebSocket server.
+
+Use this file to launch pywebsocket without Apache HTTP Server.
+
+
+BASIC USAGE
+===========
+
+Go to the src directory and run
+
+ $ python mod_pywebsocket/standalone.py [-p <ws_port>]
+ [-w <websock_handlers>]
+ [-d <document_root>]
+
+<ws_port> is the port number to use for ws:// connection.
+
+<document_root> is the path to the root directory of HTML files.
+
+<websock_handlers> is the path to the root directory of WebSocket handlers.
+If not specified, <document_root> will be used. See __init__.py (or
+run $ pydoc mod_pywebsocket) for how to write WebSocket handlers.
+
+For more detail and other options, run
+
+ $ python mod_pywebsocket/standalone.py --help
+
+or see _build_option_parser method below.
+
+For trouble shooting, adding "--log_level debug" might help you.
+
+
+TRY DEMO
+========
+
+Go to the src directory and run standalone.py with -d option to set the
+document root to the directory containing example HTMLs and handlers like this:
+
+ $ cd src
+ $ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example
+
+to launch pywebsocket with the sample handler and html on port 80. Open
+http://localhost/console.html, click the connect button, type something into
+the text box next to the send button and click the send button. If everything
+is working, you'll see the message you typed echoed by the server.
+
+
+USING TLS
+=========
+
+To run the standalone server with TLS support, run it with -t, -k, and -c
+options. When TLS is enabled, the standalone server accepts only TLS connection.
+
+Note that when ssl module is used and the key/cert location is incorrect,
+TLS connection silently fails while pyOpenSSL fails on startup.
+
+Example:
+
+ $ PYTHONPATH=. python mod_pywebsocket/standalone.py \
+ -d example \
+ -p 10443 \
+ -t \
+ -c ../test/cert/cert.pem \
+ -k ../test/cert/key.pem \
+
+Note that when passing a relative path to -c and -k option, it will be resolved
+using the document root directory as the base.
+
+
+USING CLIENT AUTHENTICATION
+===========================
+
+To run the standalone server with TLS client authentication support, run it with
+--tls-client-auth and --tls-client-ca options in addition to ones required for
+TLS support.
+
+Example:
+
+ $ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example -p 10443 -t \
+ -c ../test/cert/cert.pem -k ../test/cert/key.pem \
+ --tls-client-auth \
+ --tls-client-ca=../test/cert/cacert.pem
+
+Note that when passing a relative path to --tls-client-ca option, it will be
+resolved using the document root directory as the base.
+
+
+CONFIGURATION FILE
+==================
+
+You can also write a configuration file and use it by specifying the path to
+the configuration file by --config option. Please write a configuration file
+following the documentation of the Python ConfigParser library. Name of each
+entry must be the long version argument name. E.g. to set log level to debug,
+add the following line:
+
+log_level=debug
+
+For options which doesn't take value, please add some fake value. E.g. for
+--tls option, add the following line:
+
+tls=True
+
+Note that tls will be enabled even if you write tls=False as the value part is
+fake.
+
+When both a command line argument and a configuration file entry are set for
+the same configuration item, the command line value will override one in the
+configuration file.
+
+
+THREADING
+=========
+
+This server is derived from SocketServer.ThreadingMixIn. Hence a thread is
+used for each request.
+
+
+SECURITY WARNING
+================
+
+This uses CGIHTTPServer and CGIHTTPServer is not secure.
+It may execute arbitrary Python code or external programs. It should not be
+used outside a firewall.
+"""
+
+import BaseHTTPServer
+import CGIHTTPServer
+import SimpleHTTPServer
+import SocketServer
+import ConfigParser
+import base64
+import httplib
+import logging
+import logging.handlers
+import optparse
+import os
+import re
+import select
+import socket
+import sys
+import threading
+import time
+
+from mod_pywebsocket import common
+from mod_pywebsocket import dispatch
+from mod_pywebsocket import handshake
+from mod_pywebsocket import http_header_util
+from mod_pywebsocket import memorizingfile
+from mod_pywebsocket import util
+from mod_pywebsocket.xhr_benchmark_handler import XHRBenchmarkHandler
+
+
+_DEFAULT_LOG_MAX_BYTES = 1024 * 256
+_DEFAULT_LOG_BACKUP_COUNT = 5
+
+_DEFAULT_REQUEST_QUEUE_SIZE = 128
+
+# 1024 is practically large enough to contain WebSocket handshake lines.
+_MAX_MEMORIZED_LINES = 1024
+
+# Constants for the --tls_module flag.
+_TLS_BY_STANDARD_MODULE = 'ssl'
+_TLS_BY_PYOPENSSL = 'pyopenssl'
+
+
+class _StandaloneConnection(object):
+ """Mimic mod_python mp_conn."""
+
+ def __init__(self, request_handler):
+ """Construct an instance.
+
+ Args:
+ request_handler: A WebSocketRequestHandler instance.
+ """
+
+ self._request_handler = request_handler
+
+ def get_local_addr(self):
+ """Getter to mimic mp_conn.local_addr."""
+
+ return (self._request_handler.server.server_name,
+ self._request_handler.server.server_port)
+ local_addr = property(get_local_addr)
+
+ def get_remote_addr(self):
+ """Getter to mimic mp_conn.remote_addr.
+
+ Setting the property in __init__ won't work because the request
+ handler is not initialized yet there."""
+
+ return self._request_handler.client_address
+ remote_addr = property(get_remote_addr)
+
+ def write(self, data):
+ """Mimic mp_conn.write()."""
+
+ return self._request_handler.wfile.write(data)
+
+ def read(self, length):
+ """Mimic mp_conn.read()."""
+
+ return self._request_handler.rfile.read(length)
+
+ def get_memorized_lines(self):
+ """Get memorized lines."""
+
+ return self._request_handler.rfile.get_memorized_lines()
+
+
+class _StandaloneRequest(object):
+ """Mimic mod_python request."""
+
+ def __init__(self, request_handler, use_tls):
+ """Construct an instance.
+
+ Args:
+ request_handler: A WebSocketRequestHandler instance.
+ """
+
+ self._logger = util.get_class_logger(self)
+
+ self._request_handler = request_handler
+ self.connection = _StandaloneConnection(request_handler)
+ self._use_tls = use_tls
+ self.headers_in = request_handler.headers
+
+ def get_uri(self):
+ """Getter to mimic request.uri.
+
+ This method returns the raw data at the Request-URI part of the
+ Request-Line, while the uri method on the request object of mod_python
+ returns the path portion after parsing the raw data. This behavior is
+ kept for compatibility.
+ """
+
+ return self._request_handler.path
+ uri = property(get_uri)
+
+ def get_unparsed_uri(self):
+ """Getter to mimic request.unparsed_uri."""
+
+ return self._request_handler.path
+ unparsed_uri = property(get_unparsed_uri)
+
+ def get_method(self):
+ """Getter to mimic request.method."""
+
+ return self._request_handler.command
+ method = property(get_method)
+
+ def get_protocol(self):
+ """Getter to mimic request.protocol."""
+
+ return self._request_handler.request_version
+ protocol = property(get_protocol)
+
+ def is_https(self):
+ """Mimic request.is_https()."""
+
+ return self._use_tls
+
+
+def _import_ssl():
+ global ssl
+ try:
+ import ssl
+ return True
+ except ImportError:
+ return False
+
+
+def _import_pyopenssl():
+ global OpenSSL
+ try:
+ import OpenSSL.SSL
+ return True
+ except ImportError:
+ return False
+
+
+class _StandaloneSSLConnection(object):
+ """A wrapper class for OpenSSL.SSL.Connection to
+ - provide makefile method which is not supported by the class
+ - tweak shutdown method since OpenSSL.SSL.Connection.shutdown doesn't
+ accept the "how" argument.
+ - convert SysCallError exceptions that its recv method may raise into a
+ return value of '', meaning EOF. We cannot overwrite the recv method on
+ self._connection since it's immutable.
+ """
+
+ _OVERRIDDEN_ATTRIBUTES = ['_connection', 'makefile', 'shutdown', 'recv']
+
+ def __init__(self, connection):
+ self._connection = connection
+
+ def __getattribute__(self, name):
+ if name in _StandaloneSSLConnection._OVERRIDDEN_ATTRIBUTES:
+ return object.__getattribute__(self, name)
+ return self._connection.__getattribute__(name)
+
+ def __setattr__(self, name, value):
+ if name in _StandaloneSSLConnection._OVERRIDDEN_ATTRIBUTES:
+ return object.__setattr__(self, name, value)
+ return self._connection.__setattr__(name, value)
+
+ def makefile(self, mode='r', bufsize=-1):
+ return socket._fileobject(self, mode, bufsize)
+
+ def shutdown(self, unused_how):
+ self._connection.shutdown()
+
+ def recv(self, bufsize, flags=0):
+ if flags != 0:
+ raise ValueError('Non-zero flags not allowed')
+
+ try:
+ return self._connection.recv(bufsize)
+ except OpenSSL.SSL.SysCallError, (err, message):
+ if err == -1:
+ # Suppress "unexpected EOF" exception. See the OpenSSL document
+ # for SSL_get_error.
+ return ''
+ raise
+
+
+def _alias_handlers(dispatcher, websock_handlers_map_file):
+ """Set aliases specified in websock_handler_map_file in dispatcher.
+
+ Args:
+ dispatcher: dispatch.Dispatcher instance
+ websock_handler_map_file: alias map file
+ """
+
+ fp = open(websock_handlers_map_file)
+ try:
+ for line in fp:
+ if line[0] == '#' or line.isspace():
+ continue
+ m = re.match('(\S+)\s+(\S+)', line)
+ if not m:
+ logging.warning('Wrong format in map file:' + line)
+ continue
+ try:
+ dispatcher.add_resource_path_alias(
+ m.group(1), m.group(2))
+ except dispatch.DispatchException, e:
+ logging.error(str(e))
+ finally:
+ fp.close()
+
+
+class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
+ """HTTPServer specialized for WebSocket."""
+
+ # Overrides SocketServer.ThreadingMixIn.daemon_threads
+ daemon_threads = True
+ # Overrides BaseHTTPServer.HTTPServer.allow_reuse_address
+ allow_reuse_address = True
+
+ def __init__(self, options):
+ """Override SocketServer.TCPServer.__init__ to set SSL enabled
+ socket object to self.socket before server_bind and server_activate,
+ if necessary.
+ """
+
+ # Share a Dispatcher among request handlers to save time for
+ # instantiation. Dispatcher can be shared because it is thread-safe.
+ options.dispatcher = dispatch.Dispatcher(
+ options.websock_handlers,
+ options.scan_dir,
+ options.allow_handlers_outside_root_dir)
+ if options.websock_handlers_map_file:
+ _alias_handlers(options.dispatcher,
+ options.websock_handlers_map_file)
+ warnings = options.dispatcher.source_warnings()
+ if warnings:
+ for warning in warnings:
+ logging.warning('Warning in source loading: %s' % warning)
+
+ self._logger = util.get_class_logger(self)
+
+ self.request_queue_size = options.request_queue_size
+ self.__ws_is_shut_down = threading.Event()
+ self.__ws_serving = False
+
+ SocketServer.BaseServer.__init__(
+ self, (options.server_host, options.port), WebSocketRequestHandler)
+
+ # Expose the options object to allow handler objects access it. We name
+ # it with websocket_ prefix to avoid conflict.
+ self.websocket_server_options = options
+
+ self._create_sockets()
+ self.server_bind()
+ self.server_activate()
+
+ def _create_sockets(self):
+ self.server_name, self.server_port = self.server_address
+ self._sockets = []
+ if not self.server_name:
+ # On platforms that doesn't support IPv6, the first bind fails.
+ # On platforms that supports IPv6
+ # - If it binds both IPv4 and IPv6 on call with AF_INET6, the
+ # first bind succeeds and the second fails (we'll see 'Address
+ # already in use' error).
+ # - If it binds only IPv6 on call with AF_INET6, both call are
+ # expected to succeed to listen both protocol.
+ addrinfo_array = [
+ (socket.AF_INET6, socket.SOCK_STREAM, '', '', ''),
+ (socket.AF_INET, socket.SOCK_STREAM, '', '', '')]
+ else:
+ addrinfo_array = socket.getaddrinfo(self.server_name,
+ self.server_port,
+ socket.AF_UNSPEC,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP)
+ for addrinfo in addrinfo_array:
+ self._logger.info('Create socket on: %r', addrinfo)
+ family, socktype, proto, canonname, sockaddr = addrinfo
+ try:
+ socket_ = socket.socket(family, socktype)
+ except Exception, e:
+ self._logger.info('Skip by failure: %r', e)
+ continue
+ server_options = self.websocket_server_options
+ if server_options.use_tls:
+ # For the case of _HAS_OPEN_SSL, we do wrapper setup after
+ # accept.
+ if server_options.tls_module == _TLS_BY_STANDARD_MODULE:
+ if server_options.tls_client_auth:
+ if server_options.tls_client_cert_optional:
+ client_cert_ = ssl.CERT_OPTIONAL
+ else:
+ client_cert_ = ssl.CERT_REQUIRED
+ else:
+ client_cert_ = ssl.CERT_NONE
+ socket_ = ssl.wrap_socket(socket_,
+ keyfile=server_options.private_key,
+ certfile=server_options.certificate,
+ ssl_version=ssl.PROTOCOL_SSLv23,
+ ca_certs=server_options.tls_client_ca,
+ cert_reqs=client_cert_,
+ do_handshake_on_connect=False)
+ self._sockets.append((socket_, addrinfo))
+
+ def server_bind(self):
+ """Override SocketServer.TCPServer.server_bind to enable multiple
+ sockets bind.
+ """
+
+ failed_sockets = []
+
+ for socketinfo in self._sockets:
+ socket_, addrinfo = socketinfo
+ self._logger.info('Bind on: %r', addrinfo)
+ if self.allow_reuse_address:
+ socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ try:
+ socket_.bind(self.server_address)
+ except Exception, e:
+ self._logger.info('Skip by failure: %r', e)
+ socket_.close()
+ failed_sockets.append(socketinfo)
+ if self.server_address[1] == 0:
+ # The operating system assigns the actual port number for port
+ # number 0. This case, the second and later sockets should use
+ # the same port number. Also self.server_port is rewritten
+ # because it is exported, and will be used by external code.
+ self.server_address = (
+ self.server_name, socket_.getsockname()[1])
+ self.server_port = self.server_address[1]
+ self._logger.info('Port %r is assigned', self.server_port)
+
+ for socketinfo in failed_sockets:
+ self._sockets.remove(socketinfo)
+
+ def server_activate(self):
+ """Override SocketServer.TCPServer.server_activate to enable multiple
+ sockets listen.
+ """
+
+ failed_sockets = []
+
+ for socketinfo in self._sockets:
+ socket_, addrinfo = socketinfo
+ self._logger.info('Listen on: %r', addrinfo)
+ try:
+ socket_.listen(self.request_queue_size)
+ except Exception, e:
+ self._logger.info('Skip by failure: %r', e)
+ socket_.close()
+ failed_sockets.append(socketinfo)
+
+ for socketinfo in failed_sockets:
+ self._sockets.remove(socketinfo)
+
+ if len(self._sockets) == 0:
+ self._logger.critical(
+ 'No sockets activated. Use info log level to see the reason.')
+
+ def server_close(self):
+ """Override SocketServer.TCPServer.server_close to enable multiple
+ sockets close.
+ """
+
+ for socketinfo in self._sockets:
+ socket_, addrinfo = socketinfo
+ self._logger.info('Close on: %r', addrinfo)
+ socket_.close()
+
+ def fileno(self):
+ """Override SocketServer.TCPServer.fileno."""
+
+ self._logger.critical('Not supported: fileno')
+ return self._sockets[0][0].fileno()
+
+ def handle_error(self, request, client_address):
+ """Override SocketServer.handle_error."""
+
+ self._logger.error(
+ 'Exception in processing request from: %r\n%s',
+ client_address,
+ util.get_stack_trace())
+ # Note: client_address is a tuple.
+
+ def get_request(self):
+ """Override TCPServer.get_request to wrap OpenSSL.SSL.Connection
+ object with _StandaloneSSLConnection to provide makefile method. We
+ cannot substitute OpenSSL.SSL.Connection.makefile since it's readonly
+ attribute.
+ """
+
+ accepted_socket, client_address = self.socket.accept()
+
+ server_options = self.websocket_server_options
+ if server_options.use_tls:
+ if server_options.tls_module == _TLS_BY_STANDARD_MODULE:
+ try:
+ accepted_socket.do_handshake()
+ except ssl.SSLError, e:
+ self._logger.debug('%r', e)
+ raise
+
+ # Print cipher in use. Handshake is done on accept.
+ self._logger.debug('Cipher: %s', accepted_socket.cipher())
+ self._logger.debug('Client cert: %r',
+ accepted_socket.getpeercert())
+ elif server_options.tls_module == _TLS_BY_PYOPENSSL:
+ # We cannot print the cipher in use. pyOpenSSL doesn't provide
+ # any method to fetch that.
+
+ ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
+ ctx.use_privatekey_file(server_options.private_key)
+ ctx.use_certificate_file(server_options.certificate)
+
+ def default_callback(conn, cert, errnum, errdepth, ok):
+ return ok == 1
+
+ # See the OpenSSL document for SSL_CTX_set_verify.
+ if server_options.tls_client_auth:
+ verify_mode = OpenSSL.SSL.VERIFY_PEER
+ if not server_options.tls_client_cert_optional:
+ verify_mode |= OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT
+ ctx.set_verify(verify_mode, default_callback)
+ ctx.load_verify_locations(server_options.tls_client_ca,
+ None)
+ else:
+ ctx.set_verify(OpenSSL.SSL.VERIFY_NONE, default_callback)
+
+ accepted_socket = OpenSSL.SSL.Connection(ctx, accepted_socket)
+ accepted_socket.set_accept_state()
+
+ # Convert SSL related error into socket.error so that
+ # SocketServer ignores them and keeps running.
+ #
+ # TODO(tyoshino): Convert all kinds of errors.
+ try:
+ accepted_socket.do_handshake()
+ except OpenSSL.SSL.Error, e:
+ # Set errno part to 1 (SSL_ERROR_SSL) like the ssl module
+ # does.
+ self._logger.debug('%r', e)
+ raise socket.error(1, '%r' % e)
+ cert = accepted_socket.get_peer_certificate()
+ if cert is not None:
+ self._logger.debug('Client cert subject: %r',
+ cert.get_subject().get_components())
+ accepted_socket = _StandaloneSSLConnection(accepted_socket)
+ else:
+ raise ValueError('No TLS support module is available')
+
+ return accepted_socket, client_address
+
+ def serve_forever(self, poll_interval=0.5):
+ """Override SocketServer.BaseServer.serve_forever."""
+
+ self.__ws_serving = True
+ self.__ws_is_shut_down.clear()
+ handle_request = self.handle_request
+ if hasattr(self, '_handle_request_noblock'):
+ handle_request = self._handle_request_noblock
+ else:
+ self._logger.warning('Fallback to blocking request handler')
+ try:
+ while self.__ws_serving:
+ r, w, e = select.select(
+ [socket_[0] for socket_ in self._sockets],
+ [], [], poll_interval)
+ for socket_ in r:
+ self.socket = socket_
+ handle_request()
+ self.socket = None
+ finally:
+ self.__ws_is_shut_down.set()
+
+ def shutdown(self):
+ """Override SocketServer.BaseServer.shutdown."""
+
+ self.__ws_serving = False
+ self.__ws_is_shut_down.wait()
+
+
+class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
+ """CGIHTTPRequestHandler specialized for WebSocket."""
+
+ # Use httplib.HTTPMessage instead of mimetools.Message.
+ MessageClass = httplib.HTTPMessage
+
+ protocol_version = "HTTP/1.1"
+
+ def setup(self):
+ """Override SocketServer.StreamRequestHandler.setup to wrap rfile
+ with MemorizingFile.
+
+ This method will be called by BaseRequestHandler's constructor
+ before calling BaseHTTPRequestHandler.handle.
+ BaseHTTPRequestHandler.handle will call
+ BaseHTTPRequestHandler.handle_one_request and it will call
+ WebSocketRequestHandler.parse_request.
+ """
+
+ # Call superclass's setup to prepare rfile, wfile, etc. See setup
+ # definition on the root class SocketServer.StreamRequestHandler to
+ # understand what this does.
+ CGIHTTPServer.CGIHTTPRequestHandler.setup(self)
+
+ self.rfile = memorizingfile.MemorizingFile(
+ self.rfile,
+ max_memorized_lines=_MAX_MEMORIZED_LINES)
+
+ def __init__(self, request, client_address, server):
+ self._logger = util.get_class_logger(self)
+
+ self._options = server.websocket_server_options
+
+ # Overrides CGIHTTPServerRequestHandler.cgi_directories.
+ self.cgi_directories = self._options.cgi_directories
+ # Replace CGIHTTPRequestHandler.is_executable method.
+ if self._options.is_executable_method is not None:
+ self.is_executable = self._options.is_executable_method
+
+ # This actually calls BaseRequestHandler.__init__.
+ CGIHTTPServer.CGIHTTPRequestHandler.__init__(
+ self, request, client_address, server)
+
+ def parse_request(self):
+ """Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request.
+
+ Return True to continue processing for HTTP(S), False otherwise.
+
+ See BaseHTTPRequestHandler.handle_one_request method which calls
+ this method to understand how the return value will be handled.
+ """
+
+ # We hook parse_request method, but also call the original
+ # CGIHTTPRequestHandler.parse_request since when we return False,
+ # CGIHTTPRequestHandler.handle_one_request continues processing and
+ # it needs variables set by CGIHTTPRequestHandler.parse_request.
+ #
+ # Variables set by this method will be also used by WebSocket request
+ # handling (self.path, self.command, self.requestline, etc. See also
+ # how _StandaloneRequest's members are implemented using these
+ # attributes).
+ if not CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self):
+ return False
+
+ if self.command == "CONNECT":
+ self.send_response(200, "Connected")
+ self.send_header("Connection", "keep-alive")
+ self.end_headers()
+ return False
+
+ if self._options.use_basic_auth:
+ auth = self.headers.getheader('Authorization')
+ if auth != self._options.basic_auth_credential:
+ self.send_response(401)
+ self.send_header('WWW-Authenticate',
+ 'Basic realm="Pywebsocket"')
+ self.end_headers()
+ self._logger.info('Request basic authentication')
+ return False
+
+ host, port, resource = http_header_util.parse_uri(self.path)
+
+ # Special paths for XMLHttpRequest benchmark
+ xhr_benchmark_helper_prefix = '/073be001e10950692ccbf3a2ad21c245'
+ if resource == (xhr_benchmark_helper_prefix + '_send'):
+ xhr_benchmark_handler = XHRBenchmarkHandler(
+ self.headers, self.rfile, self.wfile)
+ xhr_benchmark_handler.do_send()
+ return False
+ if resource == (xhr_benchmark_helper_prefix + '_receive'):
+ xhr_benchmark_handler = XHRBenchmarkHandler(
+ self.headers, self.rfile, self.wfile)
+ xhr_benchmark_handler.do_receive()
+ return False
+
+ if resource is None:
+ self._logger.info('Invalid URI: %r', self.path)
+ self._logger.info('Fallback to CGIHTTPRequestHandler')
+ return True
+ server_options = self.server.websocket_server_options
+ if host is not None:
+ validation_host = server_options.validation_host
+ if validation_host is not None and host != validation_host:
+ self._logger.info('Invalid host: %r (expected: %r)',
+ host,
+ validation_host)
+ self._logger.info('Fallback to CGIHTTPRequestHandler')
+ return True
+ if port is not None:
+ validation_port = server_options.validation_port
+ if validation_port is not None and port != validation_port:
+ self._logger.info('Invalid port: %r (expected: %r)',
+ port,
+ validation_port)
+ self._logger.info('Fallback to CGIHTTPRequestHandler')
+ return True
+ self.path = resource
+
+ request = _StandaloneRequest(self, self._options.use_tls)
+
+ try:
+ # Fallback to default http handler for request paths for which
+ # we don't have request handlers.
+ if not self._options.dispatcher.get_handler_suite(self.path):
+ self._logger.info('No handler for resource: %r',
+ self.path)
+ self._logger.info('Fallback to CGIHTTPRequestHandler')
+ return True
+ except dispatch.DispatchException, e:
+ self._logger.info('Dispatch failed for error: %s', e)
+ self.send_error(e.status)
+ return False
+
+ # If any Exceptions without except clause setup (including
+ # DispatchException) is raised below this point, it will be caught
+ # and logged by WebSocketServer.
+
+ try:
+ try:
+ handshake.do_handshake(
+ request,
+ self._options.dispatcher,
+ allowDraft75=self._options.allow_draft75,
+ strict=self._options.strict)
+ except handshake.VersionException, e:
+ self._logger.info('Handshake failed for version error: %s', e)
+ self.send_response(common.HTTP_STATUS_BAD_REQUEST)
+ self.send_header(common.SEC_WEBSOCKET_VERSION_HEADER,
+ e.supported_versions)
+ self.end_headers()
+ return False
+ except handshake.HandshakeException, e:
+ # Handshake for ws(s) failed.
+ self._logger.info('Handshake failed for error: %s', e)
+ self.send_error(e.status)
+ return False
+
+ request._dispatcher = self._options.dispatcher
+ self._options.dispatcher.transfer_data(request)
+ except handshake.AbortedByUserException, e:
+ self._logger.info('Aborted: %s', e)
+ return False
+
+ def log_request(self, code='-', size='-'):
+ """Override BaseHTTPServer.log_request."""
+
+ self._logger.info('"%s" %s %s',
+ self.requestline, str(code), str(size))
+
+ def log_error(self, *args):
+ """Override BaseHTTPServer.log_error."""
+
+ # Despite the name, this method is for warnings than for errors.
+ # For example, HTTP status code is logged by this method.
+ self._logger.warning('%s - %s',
+ self.address_string(),
+ args[0] % args[1:])
+
+ def is_cgi(self):
+ """Test whether self.path corresponds to a CGI script.
+
+ Add extra check that self.path doesn't contains ..
+ Also check if the file is a executable file or not.
+ If the file is not executable, it is handled as static file or dir
+ rather than a CGI script.
+ """
+
+ if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self):
+ if '..' in self.path:
+ return False
+ # strip query parameter from request path
+ resource_name = self.path.split('?', 2)[0]
+ # convert resource_name into real path name in filesystem.
+ scriptfile = self.translate_path(resource_name)
+ if not os.path.isfile(scriptfile):
+ return False
+ if not self.is_executable(scriptfile):
+ return False
+ return True
+ return False
+
+
+def _get_logger_from_class(c):
+ return logging.getLogger('%s.%s' % (c.__module__, c.__name__))
+
+
+def _configure_logging(options):
+ logging.addLevelName(common.LOGLEVEL_FINE, 'FINE')
+
+ logger = logging.getLogger()
+ logger.setLevel(logging.getLevelName(options.log_level.upper()))
+ if options.log_file:
+ handler = logging.handlers.RotatingFileHandler(
+ options.log_file, 'a', options.log_max, options.log_count)
+ else:
+ handler = logging.StreamHandler()
+ formatter = logging.Formatter(
+ '[%(asctime)s] [%(levelname)s] %(name)s: %(message)s')
+ handler.setFormatter(formatter)
+ logger.addHandler(handler)
+
+ deflate_log_level_name = logging.getLevelName(
+ options.deflate_log_level.upper())
+ _get_logger_from_class(util._Deflater).setLevel(
+ deflate_log_level_name)
+ _get_logger_from_class(util._Inflater).setLevel(
+ deflate_log_level_name)
+
+
+def _build_option_parser():
+ parser = optparse.OptionParser()
+
+ parser.add_option('--config', dest='config_file', type='string',
+ default=None,
+ help=('Path to configuration file. See the file comment '
+ 'at the top of this file for the configuration '
+ 'file format'))
+ parser.add_option('-H', '--server-host', '--server_host',
+ dest='server_host',
+ default='',
+ help='server hostname to listen to')
+ parser.add_option('-V', '--validation-host', '--validation_host',
+ dest='validation_host',
+ default=None,
+ help='server hostname to validate in absolute path.')
+ parser.add_option('-p', '--port', dest='port', type='int',
+ default=common.DEFAULT_WEB_SOCKET_PORT,
+ help='port to listen to')
+ parser.add_option('-P', '--validation-port', '--validation_port',
+ dest='validation_port', type='int',
+ default=None,
+ help='server port to validate in absolute path.')
+ parser.add_option('-w', '--websock-handlers', '--websock_handlers',
+ dest='websock_handlers',
+ default='.',
+ help=('The root directory of WebSocket handler files. '
+ 'If the path is relative, --document-root is used '
+ 'as the base.'))
+ parser.add_option('-m', '--websock-handlers-map-file',
+ '--websock_handlers_map_file',
+ dest='websock_handlers_map_file',
+ default=None,
+ help=('WebSocket handlers map file. '
+ 'Each line consists of alias_resource_path and '
+ 'existing_resource_path, separated by spaces.'))
+ parser.add_option('-s', '--scan-dir', '--scan_dir', dest='scan_dir',
+ default=None,
+ help=('Must be a directory under --websock-handlers. '
+ 'Only handlers under this directory are scanned '
+ 'and registered to the server. '
+ 'Useful for saving scan time when the handler '
+ 'root directory contains lots of files that are '
+ 'not handler file or are handler files but you '
+ 'don\'t want them to be registered. '))
+ parser.add_option('--allow-handlers-outside-root-dir',
+ '--allow_handlers_outside_root_dir',
+ dest='allow_handlers_outside_root_dir',
+ action='store_true',
+ default=False,
+ help=('Scans WebSocket handlers even if their canonical '
+ 'path is not under --websock-handlers.'))
+ parser.add_option('-d', '--document-root', '--document_root',
+ dest='document_root', default='.',
+ help='Document root directory.')
+ parser.add_option('-x', '--cgi-paths', '--cgi_paths', dest='cgi_paths',
+ default=None,
+ help=('CGI paths relative to document_root.'
+ 'Comma-separated. (e.g -x /cgi,/htbin) '
+ 'Files under document_root/cgi_path are handled '
+ 'as CGI programs. Must be executable.'))
+ parser.add_option('-t', '--tls', dest='use_tls', action='store_true',
+ default=False, help='use TLS (wss://)')
+ parser.add_option('--tls-module', '--tls_module', dest='tls_module',
+ type='choice',
+ choices = [_TLS_BY_STANDARD_MODULE, _TLS_BY_PYOPENSSL],
+ help='Use ssl module if "%s" is specified. '
+ 'Use pyOpenSSL module if "%s" is specified' %
+ (_TLS_BY_STANDARD_MODULE, _TLS_BY_PYOPENSSL))
+ parser.add_option('-k', '--private-key', '--private_key',
+ dest='private_key',
+ default='', help='TLS private key file.')
+ parser.add_option('-c', '--certificate', dest='certificate',
+ default='', help='TLS certificate file.')
+ parser.add_option('--tls-client-auth', dest='tls_client_auth',
+ action='store_true', default=False,
+ help='Requests TLS client auth on every connection.')
+ parser.add_option('--tls-client-cert-optional',
+ dest='tls_client_cert_optional',
+ action='store_true', default=False,
+ help=('Makes client certificate optional even though '
+ 'TLS client auth is enabled.'))
+ parser.add_option('--tls-client-ca', dest='tls_client_ca', default='',
+ help=('Specifies a pem file which contains a set of '
+ 'concatenated CA certificates which are used to '
+ 'validate certificates passed from clients'))
+ parser.add_option('--basic-auth', dest='use_basic_auth',
+ action='store_true', default=False,
+ help='Requires Basic authentication.')
+ parser.add_option('--basic-auth-credential',
+ dest='basic_auth_credential', default='test:test',
+ help='Specifies the credential of basic authentication '
+ 'by username:password pair (e.g. test:test).')
+ parser.add_option('-l', '--log-file', '--log_file', dest='log_file',
+ default='', help='Log file.')
+ # Custom log level:
+ # - FINE: Prints status of each frame processing step
+ parser.add_option('--log-level', '--log_level', type='choice',
+ dest='log_level', default='warn',
+ choices=['fine',
+ 'debug', 'info', 'warning', 'warn', 'error',
+ 'critical'],
+ help='Log level.')
+ parser.add_option('--deflate-log-level', '--deflate_log_level',
+ type='choice',
+ dest='deflate_log_level', default='warn',
+ choices=['debug', 'info', 'warning', 'warn', 'error',
+ 'critical'],
+ help='Log level for _Deflater and _Inflater.')
+ parser.add_option('--thread-monitor-interval-in-sec',
+ '--thread_monitor_interval_in_sec',
+ dest='thread_monitor_interval_in_sec',
+ type='int', default=-1,
+ help=('If positive integer is specified, run a thread '
+ 'monitor to show the status of server threads '
+ 'periodically in the specified inteval in '
+ 'second. If non-positive integer is specified, '
+ 'disable the thread monitor.'))
+ parser.add_option('--log-max', '--log_max', dest='log_max', type='int',
+ default=_DEFAULT_LOG_MAX_BYTES,
+ help='Log maximum bytes')
+ parser.add_option('--log-count', '--log_count', dest='log_count',
+ type='int', default=_DEFAULT_LOG_BACKUP_COUNT,
+ help='Log backup count')
+ parser.add_option('--allow-draft75', dest='allow_draft75',
+ action='store_true', default=False,
+ help='Obsolete option. Ignored.')
+ parser.add_option('--strict', dest='strict', action='store_true',
+ default=False, help='Obsolete option. Ignored.')
+ parser.add_option('-q', '--queue', dest='request_queue_size', type='int',
+ default=_DEFAULT_REQUEST_QUEUE_SIZE,
+ help='request queue size')
+
+ return parser
+
+
+class ThreadMonitor(threading.Thread):
+ daemon = True
+
+ def __init__(self, interval_in_sec):
+ threading.Thread.__init__(self, name='ThreadMonitor')
+
+ self._logger = util.get_class_logger(self)
+
+ self._interval_in_sec = interval_in_sec
+
+ def run(self):
+ while True:
+ thread_name_list = []
+ for thread in threading.enumerate():
+ thread_name_list.append(thread.name)
+ self._logger.info(
+ "%d active threads: %s",
+ threading.active_count(),
+ ', '.join(thread_name_list))
+ time.sleep(self._interval_in_sec)
+
+
+def _parse_args_and_config(args):
+ parser = _build_option_parser()
+
+ # First, parse options without configuration file.
+ temporary_options, temporary_args = parser.parse_args(args=args)
+ if temporary_args:
+ logging.critical(
+ 'Unrecognized positional arguments: %r', temporary_args)
+ sys.exit(1)
+
+ if temporary_options.config_file:
+ try:
+ config_fp = open(temporary_options.config_file, 'r')
+ except IOError, e:
+ logging.critical(
+ 'Failed to open configuration file %r: %r',
+ temporary_options.config_file,
+ e)
+ sys.exit(1)
+
+ config_parser = ConfigParser.SafeConfigParser()
+ config_parser.readfp(config_fp)
+ config_fp.close()
+
+ args_from_config = []
+ for name, value in config_parser.items('pywebsocket'):
+ args_from_config.append('--' + name)
+ args_from_config.append(value)
+ if args is None:
+ args = args_from_config
+ else:
+ args = args_from_config + args
+ return parser.parse_args(args=args)
+ else:
+ return temporary_options, temporary_args
+
+
+def _main(args=None):
+ """You can call this function from your own program, but please note that
+ this function has some side-effects that might affect your program. For
+ example, util.wrap_popen3_for_win use in this method replaces implementation
+ of os.popen3.
+ """
+
+ options, args = _parse_args_and_config(args=args)
+
+ os.chdir(options.document_root)
+
+ _configure_logging(options)
+
+ if options.allow_draft75:
+ logging.warning('--allow_draft75 option is obsolete.')
+
+ if options.strict:
+ logging.warning('--strict option is obsolete.')
+
+ # TODO(tyoshino): Clean up initialization of CGI related values. Move some
+ # of code here to WebSocketRequestHandler class if it's better.
+ options.cgi_directories = []
+ options.is_executable_method = None
+ if options.cgi_paths:
+ options.cgi_directories = options.cgi_paths.split(',')
+ if sys.platform in ('cygwin', 'win32'):
+ cygwin_path = None
+ # For Win32 Python, it is expected that CYGWIN_PATH
+ # is set to a directory of cygwin binaries.
+ # For example, websocket_server.py in Chromium sets CYGWIN_PATH to
+ # full path of third_party/cygwin/bin.
+ if 'CYGWIN_PATH' in os.environ:
+ cygwin_path = os.environ['CYGWIN_PATH']
+ util.wrap_popen3_for_win(cygwin_path)
+
+ def __check_script(scriptpath):
+ return util.get_script_interp(scriptpath, cygwin_path)
+
+ options.is_executable_method = __check_script
+
+ if options.use_tls:
+ if options.tls_module is None:
+ if _import_ssl():
+ options.tls_module = _TLS_BY_STANDARD_MODULE
+ logging.debug('Using ssl module')
+ elif _import_pyopenssl():
+ options.tls_module = _TLS_BY_PYOPENSSL
+ logging.debug('Using pyOpenSSL module')
+ else:
+ logging.critical(
+ 'TLS support requires ssl or pyOpenSSL module.')
+ sys.exit(1)
+ elif options.tls_module == _TLS_BY_STANDARD_MODULE:
+ if not _import_ssl():
+ logging.critical('ssl module is not available')
+ sys.exit(1)
+ elif options.tls_module == _TLS_BY_PYOPENSSL:
+ if not _import_pyopenssl():
+ logging.critical('pyOpenSSL module is not available')
+ sys.exit(1)
+ else:
+ logging.critical('Invalid --tls-module option: %r',
+ options.tls_module)
+ sys.exit(1)
+
+ if not options.private_key or not options.certificate:
+ logging.critical(
+ 'To use TLS, specify private_key and certificate.')
+ sys.exit(1)
+
+ if (options.tls_client_cert_optional and
+ not options.tls_client_auth):
+ logging.critical('Client authentication must be enabled to '
+ 'specify tls_client_cert_optional')
+ sys.exit(1)
+ else:
+ if options.tls_module is not None:
+ logging.critical('Use --tls-module option only together with '
+ '--use-tls option.')
+ sys.exit(1)
+
+ if options.tls_client_auth:
+ logging.critical('TLS must be enabled for client authentication.')
+ sys.exit(1)
+
+ if options.tls_client_cert_optional:
+ logging.critical('TLS must be enabled for client authentication.')
+ sys.exit(1)
+
+ if not options.scan_dir:
+ options.scan_dir = options.websock_handlers
+
+ if options.use_basic_auth:
+ options.basic_auth_credential = 'Basic ' + base64.b64encode(
+ options.basic_auth_credential)
+
+ try:
+ if options.thread_monitor_interval_in_sec > 0:
+ # Run a thread monitor to show the status of server threads for
+ # debugging.
+ ThreadMonitor(options.thread_monitor_interval_in_sec).start()
+
+ server = WebSocketServer(options)
+ server.serve_forever()
+ except Exception, e:
+ logging.critical('mod_pywebsocket: %s' % e)
+ logging.critical('mod_pywebsocket: %s' % util.get_stack_trace())
+ sys.exit(1)
+
+
+if __name__ == '__main__':
+ _main(sys.argv[1:])
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/stream.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/stream.py
new file mode 100644
index 000000000..edc533279
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/stream.py
@@ -0,0 +1,57 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""This file exports public symbols.
+"""
+
+
+from mod_pywebsocket._stream_base import BadOperationException
+from mod_pywebsocket._stream_base import ConnectionTerminatedException
+from mod_pywebsocket._stream_base import InvalidFrameException
+from mod_pywebsocket._stream_base import InvalidUTF8Exception
+from mod_pywebsocket._stream_base import UnsupportedFrameException
+from mod_pywebsocket._stream_hixie75 import StreamHixie75
+from mod_pywebsocket._stream_hybi import Frame
+from mod_pywebsocket._stream_hybi import Stream
+from mod_pywebsocket._stream_hybi import StreamOptions
+
+# These methods are intended to be used by WebSocket client developers to have
+# their implementations receive broken data in tests.
+from mod_pywebsocket._stream_hybi import create_close_frame
+from mod_pywebsocket._stream_hybi import create_header
+from mod_pywebsocket._stream_hybi import create_length_header
+from mod_pywebsocket._stream_hybi import create_ping_frame
+from mod_pywebsocket._stream_hybi import create_pong_frame
+from mod_pywebsocket._stream_hybi import create_binary_frame
+from mod_pywebsocket._stream_hybi import create_text_frame
+from mod_pywebsocket._stream_hybi import create_closing_handshake_body
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/util.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/util.py
new file mode 100644
index 000000000..d224ae394
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/util.py
@@ -0,0 +1,416 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""WebSocket utilities.
+"""
+
+
+import array
+import errno
+
+# Import hash classes from a module available and recommended for each Python
+# version and re-export those symbol. Use sha and md5 module in Python 2.4, and
+# hashlib module in Python 2.6.
+try:
+ import hashlib
+ md5_hash = hashlib.md5
+ sha1_hash = hashlib.sha1
+except ImportError:
+ import md5
+ import sha
+ md5_hash = md5.md5
+ sha1_hash = sha.sha
+
+import StringIO
+import logging
+import os
+import re
+import socket
+import traceback
+import zlib
+
+try:
+ from mod_pywebsocket import fast_masking
+except ImportError:
+ pass
+
+
+def get_stack_trace():
+ """Get the current stack trace as string.
+
+ This is needed to support Python 2.3.
+ TODO: Remove this when we only support Python 2.4 and above.
+ Use traceback.format_exc instead.
+ """
+
+ out = StringIO.StringIO()
+ traceback.print_exc(file=out)
+ return out.getvalue()
+
+
+def prepend_message_to_exception(message, exc):
+ """Prepend message to the exception."""
+
+ exc.args = (message + str(exc),)
+ return
+
+
+def __translate_interp(interp, cygwin_path):
+ """Translate interp program path for Win32 python to run cygwin program
+ (e.g. perl). Note that it doesn't support path that contains space,
+ which is typically true for Unix, where #!-script is written.
+ For Win32 python, cygwin_path is a directory of cygwin binaries.
+
+ Args:
+ interp: interp command line
+ cygwin_path: directory name of cygwin binary, or None
+ Returns:
+ translated interp command line.
+ """
+ if not cygwin_path:
+ return interp
+ m = re.match('^[^ ]*/([^ ]+)( .*)?', interp)
+ if m:
+ cmd = os.path.join(cygwin_path, m.group(1))
+ return cmd + m.group(2)
+ return interp
+
+
+def get_script_interp(script_path, cygwin_path=None):
+ """Gets #!-interpreter command line from the script.
+
+ It also fixes command path. When Cygwin Python is used, e.g. in WebKit,
+ it could run "/usr/bin/perl -wT hello.pl".
+ When Win32 Python is used, e.g. in Chromium, it couldn't. So, fix
+ "/usr/bin/perl" to "<cygwin_path>\perl.exe".
+
+ Args:
+ script_path: pathname of the script
+ cygwin_path: directory name of cygwin binary, or None
+ Returns:
+ #!-interpreter command line, or None if it is not #!-script.
+ """
+ fp = open(script_path)
+ line = fp.readline()
+ fp.close()
+ m = re.match('^#!(.*)', line)
+ if m:
+ return __translate_interp(m.group(1), cygwin_path)
+ return None
+
+
+def wrap_popen3_for_win(cygwin_path):
+ """Wrap popen3 to support #!-script on Windows.
+
+ Args:
+ cygwin_path: path for cygwin binary if command path is needed to be
+ translated. None if no translation required.
+ """
+
+ __orig_popen3 = os.popen3
+
+ def __wrap_popen3(cmd, mode='t', bufsize=-1):
+ cmdline = cmd.split(' ')
+ interp = get_script_interp(cmdline[0], cygwin_path)
+ if interp:
+ cmd = interp + ' ' + cmd
+ return __orig_popen3(cmd, mode, bufsize)
+
+ os.popen3 = __wrap_popen3
+
+
+def hexify(s):
+ return ' '.join(map(lambda x: '%02x' % ord(x), s))
+
+
+def get_class_logger(o):
+ return logging.getLogger(
+ '%s.%s' % (o.__class__.__module__, o.__class__.__name__))
+
+
+class NoopMasker(object):
+ """A masking object that has the same interface as RepeatedXorMasker but
+ just returns the string passed in without making any change.
+ """
+
+ def __init__(self):
+ pass
+
+ def mask(self, s):
+ return s
+
+
+class RepeatedXorMasker(object):
+ """A masking object that applies XOR on the string given to mask method
+ with the masking bytes given to the constructor repeatedly. This object
+ remembers the position in the masking bytes the last mask method call
+ ended and resumes from that point on the next mask method call.
+ """
+
+ def __init__(self, masking_key):
+ self._masking_key = masking_key
+ self._masking_key_index = 0
+
+ def _mask_using_swig(self, s):
+ masked_data = fast_masking.mask(
+ s, self._masking_key, self._masking_key_index)
+ self._masking_key_index = (
+ (self._masking_key_index + len(s)) % len(self._masking_key))
+ return masked_data
+
+ def _mask_using_array(self, s):
+ result = array.array('B')
+ result.fromstring(s)
+
+ # Use temporary local variables to eliminate the cost to access
+ # attributes
+ masking_key = map(ord, self._masking_key)
+ masking_key_size = len(masking_key)
+ masking_key_index = self._masking_key_index
+
+ for i in xrange(len(result)):
+ result[i] ^= masking_key[masking_key_index]
+ masking_key_index = (masking_key_index + 1) % masking_key_size
+
+ self._masking_key_index = masking_key_index
+
+ return result.tostring()
+
+ if 'fast_masking' in globals():
+ mask = _mask_using_swig
+ else:
+ mask = _mask_using_array
+
+
+# By making wbits option negative, we can suppress CMF/FLG (2 octet) and
+# ADLER32 (4 octet) fields of zlib so that we can use zlib module just as
+# deflate library. DICTID won't be added as far as we don't set dictionary.
+# LZ77 window of 32K will be used for both compression and decompression.
+# For decompression, we can just use 32K to cover any windows size. For
+# compression, we use 32K so receivers must use 32K.
+#
+# Compression level is Z_DEFAULT_COMPRESSION. We don't have to match level
+# to decode.
+#
+# See zconf.h, deflate.cc, inflate.cc of zlib library, and zlibmodule.c of
+# Python. See also RFC1950 (ZLIB 3.3).
+
+
+class _Deflater(object):
+
+ def __init__(self, window_bits):
+ self._logger = get_class_logger(self)
+
+ self._compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -window_bits)
+
+ def compress(self, bytes):
+ compressed_bytes = self._compress.compress(bytes)
+ self._logger.debug('Compress input %r', bytes)
+ self._logger.debug('Compress result %r', compressed_bytes)
+ return compressed_bytes
+
+ def compress_and_flush(self, bytes):
+ compressed_bytes = self._compress.compress(bytes)
+ compressed_bytes += self._compress.flush(zlib.Z_SYNC_FLUSH)
+ self._logger.debug('Compress input %r', bytes)
+ self._logger.debug('Compress result %r', compressed_bytes)
+ return compressed_bytes
+
+ def compress_and_finish(self, bytes):
+ compressed_bytes = self._compress.compress(bytes)
+ compressed_bytes += self._compress.flush(zlib.Z_FINISH)
+ self._logger.debug('Compress input %r', bytes)
+ self._logger.debug('Compress result %r', compressed_bytes)
+ return compressed_bytes
+
+
+class _Inflater(object):
+
+ def __init__(self, window_bits):
+ self._logger = get_class_logger(self)
+ self._window_bits = window_bits
+
+ self._unconsumed = ''
+
+ self.reset()
+
+ def decompress(self, size):
+ if not (size == -1 or size > 0):
+ raise Exception('size must be -1 or positive')
+
+ data = ''
+
+ while True:
+ if size == -1:
+ data += self._decompress.decompress(self._unconsumed)
+ # See Python bug http://bugs.python.org/issue12050 to
+ # understand why the same code cannot be used for updating
+ # self._unconsumed for here and else block.
+ self._unconsumed = ''
+ else:
+ data += self._decompress.decompress(
+ self._unconsumed, size - len(data))
+ self._unconsumed = self._decompress.unconsumed_tail
+ if self._decompress.unused_data:
+ # Encountered a last block (i.e. a block with BFINAL = 1) and
+ # found a new stream (unused_data). We cannot use the same
+ # zlib.Decompress object for the new stream. Create a new
+ # Decompress object to decompress the new one.
+ #
+ # It's fine to ignore unconsumed_tail if unused_data is not
+ # empty.
+ self._unconsumed = self._decompress.unused_data
+ self.reset()
+ if size >= 0 and len(data) == size:
+ # data is filled. Don't call decompress again.
+ break
+ else:
+ # Re-invoke Decompress.decompress to try to decompress all
+ # available bytes before invoking read which blocks until
+ # any new byte is available.
+ continue
+ else:
+ # Here, since unused_data is empty, even if unconsumed_tail is
+ # not empty, bytes of requested length are already in data. We
+ # don't have to "continue" here.
+ break
+
+ if data:
+ self._logger.debug('Decompressed %r', data)
+ return data
+
+ def append(self, data):
+ self._logger.debug('Appended %r', data)
+ self._unconsumed += data
+
+ def reset(self):
+ self._logger.debug('Reset')
+ self._decompress = zlib.decompressobj(-self._window_bits)
+
+
+# Compresses/decompresses given octets using the method introduced in RFC1979.
+
+
+class _RFC1979Deflater(object):
+ """A compressor class that applies DEFLATE to given byte sequence and
+ flushes using the algorithm described in the RFC1979 section 2.1.
+ """
+
+ def __init__(self, window_bits, no_context_takeover):
+ self._deflater = None
+ if window_bits is None:
+ window_bits = zlib.MAX_WBITS
+ self._window_bits = window_bits
+ self._no_context_takeover = no_context_takeover
+
+ def filter(self, bytes, end=True, bfinal=False):
+ if self._deflater is None:
+ self._deflater = _Deflater(self._window_bits)
+
+ if bfinal:
+ result = self._deflater.compress_and_finish(bytes)
+ # Add a padding block with BFINAL = 0 and BTYPE = 0.
+ result = result + chr(0)
+ self._deflater = None
+ return result
+
+ result = self._deflater.compress_and_flush(bytes)
+ if end:
+ # Strip last 4 octets which is LEN and NLEN field of a
+ # non-compressed block added for Z_SYNC_FLUSH.
+ result = result[:-4]
+
+ if self._no_context_takeover and end:
+ self._deflater = None
+
+ return result
+
+
+class _RFC1979Inflater(object):
+ """A decompressor class for byte sequence compressed and flushed following
+ the algorithm described in the RFC1979 section 2.1.
+ """
+
+ def __init__(self, window_bits=zlib.MAX_WBITS):
+ self._inflater = _Inflater(window_bits)
+
+ def filter(self, bytes):
+ # Restore stripped LEN and NLEN field of a non-compressed block added
+ # for Z_SYNC_FLUSH.
+ self._inflater.append(bytes + '\x00\x00\xff\xff')
+ return self._inflater.decompress(-1)
+
+
+class DeflateSocket(object):
+ """A wrapper class for socket object to intercept send and recv to perform
+ deflate compression and decompression transparently.
+ """
+
+ # Size of the buffer passed to recv to receive compressed data.
+ _RECV_SIZE = 4096
+
+ def __init__(self, socket):
+ self._socket = socket
+
+ self._logger = get_class_logger(self)
+
+ self._deflater = _Deflater(zlib.MAX_WBITS)
+ self._inflater = _Inflater(zlib.MAX_WBITS)
+
+ def recv(self, size):
+ """Receives data from the socket specified on the construction up
+ to the specified size. Once any data is available, returns it even
+ if it's smaller than the specified size.
+ """
+
+ # TODO(tyoshino): Allow call with size=0. It should block until any
+ # decompressed data is available.
+ if size <= 0:
+ raise Exception('Non-positive size passed')
+ while True:
+ data = self._inflater.decompress(size)
+ if len(data) != 0:
+ return data
+
+ read_data = self._socket.recv(DeflateSocket._RECV_SIZE)
+ if not read_data:
+ return ''
+ self._inflater.append(read_data)
+
+ def sendall(self, bytes):
+ self.send(bytes)
+
+ def send(self, bytes):
+ self._socket.sendall(self._deflater.compress_and_flush(bytes))
+ return len(bytes)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/xhr_benchmark_handler.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/xhr_benchmark_handler.py
new file mode 100644
index 000000000..6735c7e2a
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/xhr_benchmark_handler.py
@@ -0,0 +1,109 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the COPYING file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+
+from mod_pywebsocket import util
+
+
+class XHRBenchmarkHandler(object):
+ def __init__(self, headers, rfile, wfile):
+ self._logger = util.get_class_logger(self)
+
+ self.headers = headers
+ self.rfile = rfile
+ self.wfile = wfile
+
+ def do_send(self):
+ content_length = int(self.headers.getheader('Content-Length'))
+
+ self._logger.debug('Requested to receive %s bytes', content_length)
+
+ RECEIVE_BLOCK_SIZE = 1024 * 1024
+
+ bytes_to_receive = content_length
+ while bytes_to_receive > 0:
+ bytes_to_receive_in_this_loop = bytes_to_receive
+ if bytes_to_receive_in_this_loop > RECEIVE_BLOCK_SIZE:
+ bytes_to_receive_in_this_loop = RECEIVE_BLOCK_SIZE
+ received_data = self.rfile.read(bytes_to_receive_in_this_loop)
+ if received_data != ('a' * bytes_to_receive_in_this_loop):
+ self._logger.debug('Request body verification failed')
+ return
+ bytes_to_receive -= len(received_data)
+ if bytes_to_receive < 0:
+ self._logger.debug('Received %d more bytes than expected' %
+ (-bytes_to_receive))
+ return
+
+ # Return the number of received bytes back to the client.
+ response_body = '%d' % content_length
+ self.wfile.write(
+ 'HTTP/1.1 200 OK\r\n'
+ 'Content-Type: text/html\r\n'
+ 'Content-Length: %d\r\n'
+ '\r\n%s' % (len(response_body), response_body))
+ self.wfile.flush()
+
+ def do_receive(self):
+ content_length = int(self.headers.getheader('Content-Length'))
+ request_body = self.rfile.read(content_length)
+
+ request_array = request_body.split(' ')
+ if len(request_array) < 2:
+ self._logger.debug('Malformed request body: %r', request_body)
+ return
+
+ # Parse the size parameter.
+ bytes_to_send = request_array[0]
+ try:
+ bytes_to_send = int(bytes_to_send)
+ except ValueError, e:
+ self._logger.debug('Malformed size parameter: %r', bytes_to_send)
+ return
+ self._logger.debug('Requested to send %s bytes', bytes_to_send)
+
+ # Parse the transfer encoding parameter.
+ chunked_mode = False
+ mode_parameter = request_array[1]
+ if mode_parameter == 'chunked':
+ self._logger.debug('Requested chunked transfer encoding')
+ chunked_mode = True
+ elif mode_parameter != 'none':
+ self._logger.debug('Invalid mode parameter: %r', mode_parameter)
+ return
+
+ # Write a header
+ response_header = (
+ 'HTTP/1.1 200 OK\r\n'
+ 'Content-Type: application/octet-stream\r\n')
+ if chunked_mode:
+ response_header += 'Transfer-Encoding: chunked\r\n\r\n'
+ else:
+ response_header += (
+ 'Content-Length: %d\r\n\r\n' % bytes_to_send)
+ self.wfile.write(response_header)
+ self.wfile.flush()
+
+ # Write a body
+ SEND_BLOCK_SIZE = 1024 * 1024
+
+ while bytes_to_send > 0:
+ bytes_to_send_in_this_loop = bytes_to_send
+ if bytes_to_send_in_this_loop > SEND_BLOCK_SIZE:
+ bytes_to_send_in_this_loop = SEND_BLOCK_SIZE
+
+ if chunked_mode:
+ self.wfile.write('%x\r\n' % bytes_to_send_in_this_loop)
+ self.wfile.write('a' * bytes_to_send_in_this_loop)
+ if chunked_mode:
+ self.wfile.write('\r\n')
+ self.wfile.flush()
+
+ bytes_to_send -= bytes_to_send_in_this_loop
+
+ if chunked_mode:
+ self.wfile.write('0\r\n\r\n')
+ self.wfile.flush()
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/setup.py b/testing/web-platform/tests/tools/pywebsocket/src/setup.py
new file mode 100755
index 000000000..ada8db3e1
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/setup.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Set up script for mod_pywebsocket.
+"""
+
+
+from distutils.core import setup, Extension
+import sys
+
+
+_PACKAGE_NAME = 'mod_pywebsocket'
+
+# Build and use a C++ extension for faster masking. SWIG is required.
+_USE_FAST_MASKING = False
+
+if sys.version < '2.3':
+ print >> sys.stderr, '%s requires Python 2.3 or later.' % _PACKAGE_NAME
+ sys.exit(1)
+
+if _USE_FAST_MASKING:
+ setup(ext_modules=[
+ Extension(
+ 'mod_pywebsocket/_fast_masking',
+ ['mod_pywebsocket/fast_masking.i'],
+ swig_opts=['-c++'])])
+
+setup(author='Yuzo Fujishima',
+ author_email='yuzo@chromium.org',
+ description='WebSocket extension for Apache HTTP Server.',
+ long_description=(
+ 'mod_pywebsocket is an Apache HTTP Server extension for '
+ 'the WebSocket Protocol (RFC 6455). '
+ 'See mod_pywebsocket/__init__.py for more detail.'),
+ license='See COPYING',
+ name=_PACKAGE_NAME,
+ packages=[_PACKAGE_NAME, _PACKAGE_NAME + '.handshake'],
+ url='http://code.google.com/p/pywebsocket/',
+ # See the source of distutils.version, distutils.versionpredicate and
+ # distutils.dist to understand how to name version numbers.
+ version='0.7.9',
+ )
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/__init__.py b/testing/web-platform/tests/tools/pywebsocket/src/test/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/__init__.py
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cacert.pem b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cacert.pem
new file mode 100644
index 000000000..4dadae121
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cacert.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICvDCCAiWgAwIBAgIJAKqVghkGF1rSMA0GCSqGSIb3DQEBBQUAMEkxCzAJBgNV
+BAYTAkpQMQ4wDAYDVQQIEwVUb2t5bzEUMBIGA1UEChMLcHl3ZWJzb2NrZXQxFDAS
+BgNVBAMTC3B5d2Vic29ja2V0MB4XDTEyMDYwNjA3MjQzM1oXDTM5MTAyMzA3MjQz
+M1owSTELMAkGA1UEBhMCSlAxDjAMBgNVBAgTBVRva3lvMRQwEgYDVQQKEwtweXdl
+YnNvY2tldDEUMBIGA1UEAxMLcHl3ZWJzb2NrZXQwgZ8wDQYJKoZIhvcNAQEBBQAD
+gY0AMIGJAoGBAKoSEW2biQxVrMMKdn/8PJzDYiSXDPR9WQbLRRQ1Gm5jkCYiahXW
+u2CbTThfPPfi2NHA3I+HlT7gO9yR7RVUvN6ISUzGwXDEq4f4UNqtQOhQaqqK+CZ9
+LO/BhO/YYfNrbSPlYzHUKaT9ese7xO9VzVKLW+qUf2Mjh4/+SzxBDNP7AgMBAAGj
+gaswgagwHQYDVR0OBBYEFOsWdxCSuyhwaZeab6BoTho3++bzMHkGA1UdIwRyMHCA
+FOsWdxCSuyhwaZeab6BoTho3++bzoU2kSzBJMQswCQYDVQQGEwJKUDEOMAwGA1UE
+CBMFVG9reW8xFDASBgNVBAoTC3B5d2Vic29ja2V0MRQwEgYDVQQDEwtweXdlYnNv
+Y2tldIIJAKqVghkGF1rSMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEA
+gsMI1WEYqNw/jhUIdrTBcCxJ0X6hJvA9ziKANVm1Rs+4P3YDArkQ8bCr6xY+Kw7s
+Zp0yE7dM8GMdi+DU6hL3t3E5eMkTS1yZr9WCK4f2RLo+et98selZydpHemF3DJJ3
+gAj8Sx4LBaG8Cb/WnEMPv3MxG3fBE5favF6V4jU07hQ=
+-----END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cert.pem b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cert.pem
new file mode 100644
index 000000000..25379a72b
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cert.pem
@@ -0,0 +1,61 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 1 (0x1)
+ Signature Algorithm: sha1WithRSAEncryption
+ Issuer: C=JP, ST=Tokyo, O=pywebsocket, CN=pywebsocket
+ Validity
+ Not Before: Jun 6 07:25:08 2012 GMT
+ Not After : Oct 23 07:25:08 2039 GMT
+ Subject: C=JP, ST=Tokyo, O=pywebsocket, CN=pywebsocket
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public Key: (1024 bit)
+ Modulus (1024 bit):
+ 00:de:10:ce:3a:5a:04:a4:1c:29:93:5c:23:82:1a:
+ f2:06:01:e6:2b:a4:0f:dd:77:49:76:89:03:a2:21:
+ de:04:75:c6:e2:dd:fb:35:27:3a:a2:92:8e:12:62:
+ 2b:3e:1f:f4:78:df:b6:94:cb:27:d6:cb:d6:37:d7:
+ 5c:08:f0:09:3e:c9:ce:24:2d:00:c9:df:4a:e0:99:
+ e5:fb:23:a9:e2:d6:c9:3d:96:fa:01:88:de:5a:89:
+ b0:cf:03:67:6f:04:86:1d:ef:62:1c:55:a9:07:9a:
+ 2e:66:2a:73:5b:4c:62:03:f9:82:83:db:68:bf:b8:
+ 4b:0b:8b:93:11:b8:54:73:7b
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Basic Constraints:
+ CA:FALSE
+ Netscape Cert Type:
+ SSL Server
+ Netscape Comment:
+ OpenSSL Generated Certificate
+ X509v3 Subject Key Identifier:
+ 82:A1:73:8B:16:0C:7C:E4:D3:46:95:13:95:1A:32:C1:84:E9:06:00
+ X509v3 Authority Key Identifier:
+ keyid:EB:16:77:10:92:BB:28:70:69:97:9A:6F:A0:68:4E:1A:37:FB:E6:F3
+
+ Signature Algorithm: sha1WithRSAEncryption
+ 6b:b3:46:29:02:df:b0:c8:8e:c4:d7:7f:a0:1e:0d:1a:eb:2f:
+ df:d1:48:57:36:5f:95:8c:1b:f0:51:d6:52:e7:8d:84:3b:9f:
+ d8:ed:22:9c:aa:bd:ee:9b:90:1d:84:a3:4c:0b:cb:eb:64:73:
+ ba:f7:15:ce:da:5f:db:8b:15:07:a6:28:7f:b9:8c:11:9b:64:
+ d3:f1:be:52:4f:c3:d8:58:fe:de:56:63:63:3b:51:ed:a7:81:
+ f9:05:51:70:63:32:09:0e:94:7e:05:fe:a1:56:18:34:98:d5:
+ 99:1e:4e:27:38:89:90:6a:e5:ce:60:35:01:f5:de:34:60:b1:
+ cb:ae
+-----BEGIN CERTIFICATE-----
+MIICmDCCAgGgAwIBAgIBATANBgkqhkiG9w0BAQUFADBJMQswCQYDVQQGEwJKUDEO
+MAwGA1UECBMFVG9reW8xFDASBgNVBAoTC3B5d2Vic29ja2V0MRQwEgYDVQQDEwtw
+eXdlYnNvY2tldDAeFw0xMjA2MDYwNzI1MDhaFw0zOTEwMjMwNzI1MDhaMEkxCzAJ
+BgNVBAYTAkpQMQ4wDAYDVQQIEwVUb2t5bzEUMBIGA1UEChMLcHl3ZWJzb2NrZXQx
+FDASBgNVBAMTC3B5d2Vic29ja2V0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
+gQDeEM46WgSkHCmTXCOCGvIGAeYrpA/dd0l2iQOiId4Edcbi3fs1Jzqiko4SYis+
+H/R437aUyyfWy9Y311wI8Ak+yc4kLQDJ30rgmeX7I6ni1sk9lvoBiN5aibDPA2dv
+BIYd72IcVakHmi5mKnNbTGID+YKD22i/uEsLi5MRuFRzewIDAQABo4GPMIGMMAkG
+A1UdEwQCMAAwEQYJYIZIAYb4QgEBBAQDAgZAMCwGCWCGSAGG+EIBDQQfFh1PcGVu
+U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUgqFzixYMfOTTRpUT
+lRoywYTpBgAwHwYDVR0jBBgwFoAU6xZ3EJK7KHBpl5pvoGhOGjf75vMwDQYJKoZI
+hvcNAQEFBQADgYEAa7NGKQLfsMiOxNd/oB4NGusv39FIVzZflYwb8FHWUueNhDuf
+2O0inKq97puQHYSjTAvL62RzuvcVztpf24sVB6Yof7mMEZtk0/G+Uk/D2Fj+3lZj
+YztR7aeB+QVRcGMyCQ6UfgX+oVYYNJjVmR5OJziJkGrlzmA1AfXeNGCxy64=
+-----END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/cert/client_cert.p12 b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/client_cert.p12
new file mode 100644
index 000000000..14e139927
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/client_cert.p12
Binary files differ
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/cert/key.pem b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/key.pem
new file mode 100644
index 000000000..fae858318
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/key.pem
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXgIBAAKBgQDeEM46WgSkHCmTXCOCGvIGAeYrpA/dd0l2iQOiId4Edcbi3fs1
+Jzqiko4SYis+H/R437aUyyfWy9Y311wI8Ak+yc4kLQDJ30rgmeX7I6ni1sk9lvoB
+iN5aibDPA2dvBIYd72IcVakHmi5mKnNbTGID+YKD22i/uEsLi5MRuFRzewIDAQAB
+AoGBAIuCuV1Vcnb7rm8CwtgZP5XgmY8vSjxTldafa6XvawEYUTP0S77v/1llg1Yv
+UIV+I+PQgG9oVoYOl22LoimHS/Z3e1fsot5tDYszGe8/Gkst4oaReSoxvBUa6WXp
+QSo7YFCajuHtE+W/gzF+UHbdzzXIDjQZ314LNF5t+4UnsEPBAkEA+girImqWoM2t
+3UR8f8oekERwsmEMf9DH5YpH4cvUnvI+kwesC/r2U8Sho++fyEMUNm7aIXGqNLga
+ogAM+4NX4QJBAONdSxSay22egTGNoIhLndljWkuOt/9FWj2klf/4QxD4blMJQ5Oq
+QdOGAh7nVQjpPLQ5D7CBVAKpGM2CD+QJBtsCQEP2kz35pxPylG3urcC2mfQxBkkW
+ZCViBNP58GwJ0bOauTOSBEwFXWuLqTw8aDwxL49UNmqc0N0fpe2fAehj3UECQQCm
+FH/DjU8Lw7ybddjNtm6XXPuYNagxz3cbkB4B3FchDleIUDwMoVF0MW9bI5/54mV1
+QDk1tUKortxvQZJaAD4BAkEAhGOHQqPd6bBBoFBvpaLzPJMxwLKrB+Wtkq/QlC72
+ClRiMn2g8SALiIL3BDgGXKcKE/Wy7jo/af/JCzQ/cPqt/A==
+-----END RSA PRIVATE KEY-----
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/client_for_testing.py b/testing/web-platform/tests/tools/pywebsocket/src/test/client_for_testing.py
new file mode 100644
index 000000000..c7f805ee9
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/client_for_testing.py
@@ -0,0 +1,1100 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""WebSocket client utility for testing.
+
+This module contains helper methods for performing handshake, frame
+sending/receiving as a WebSocket client.
+
+This is code for testing mod_pywebsocket. Keep this code independent from
+mod_pywebsocket. Don't import e.g. Stream class for generating frame for
+testing. Using util.hexify, etc. that are not related to protocol processing
+is allowed.
+
+Note:
+This code is far from robust, e.g., we cut corners in handshake.
+"""
+
+
+import base64
+import errno
+import logging
+import os
+import random
+import re
+import socket
+import struct
+import time
+
+from mod_pywebsocket import common
+from mod_pywebsocket import util
+
+
+DEFAULT_PORT = 80
+DEFAULT_SECURE_PORT = 443
+
+# Opcodes introduced in IETF HyBi 01 for the new framing format
+OPCODE_CONTINUATION = 0x0
+OPCODE_CLOSE = 0x8
+OPCODE_PING = 0x9
+OPCODE_PONG = 0xa
+OPCODE_TEXT = 0x1
+OPCODE_BINARY = 0x2
+
+# Strings used for handshake
+_UPGRADE_HEADER = 'Upgrade: websocket\r\n'
+_UPGRADE_HEADER_HIXIE75 = 'Upgrade: WebSocket\r\n'
+_CONNECTION_HEADER = 'Connection: Upgrade\r\n'
+
+WEBSOCKET_ACCEPT_UUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
+
+# Status codes
+STATUS_NORMAL_CLOSURE = 1000
+STATUS_GOING_AWAY = 1001
+STATUS_PROTOCOL_ERROR = 1002
+STATUS_UNSUPPORTED_DATA = 1003
+STATUS_NO_STATUS_RECEIVED = 1005
+STATUS_ABNORMAL_CLOSURE = 1006
+STATUS_INVALID_FRAME_PAYLOAD_DATA = 1007
+STATUS_POLICY_VIOLATION = 1008
+STATUS_MESSAGE_TOO_BIG = 1009
+STATUS_MANDATORY_EXT = 1010
+STATUS_INTERNAL_ENDPOINT_ERROR = 1011
+STATUS_TLS_HANDSHAKE = 1015
+
+# Extension tokens
+_DEFLATE_FRAME_EXTENSION = 'deflate-frame'
+# TODO(bashi): Update after mux implementation finished.
+_MUX_EXTENSION = 'mux_DO_NOT_USE'
+_PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate'
+
+def _method_line(resource):
+ return 'GET %s HTTP/1.1\r\n' % resource
+
+
+def _sec_origin_header(origin):
+ return 'Sec-WebSocket-Origin: %s\r\n' % origin.lower()
+
+
+def _origin_header(origin):
+ # 4.1 13. concatenation of the string "Origin:", a U+0020 SPACE character,
+ # and the /origin/ value, converted to ASCII lowercase, to /fields/.
+ return 'Origin: %s\r\n' % origin.lower()
+
+
+def _format_host_header(host, port, secure):
+ # 4.1 9. Let /hostport/ be an empty string.
+ # 4.1 10. Append the /host/ value, converted to ASCII lowercase, to
+ # /hostport/
+ hostport = host.lower()
+ # 4.1 11. If /secure/ is false, and /port/ is not 80, or if /secure/
+ # is true, and /port/ is not 443, then append a U+003A COLON character
+ # (:) followed by the value of /port/, expressed as a base-ten integer,
+ # to /hostport/
+ if ((not secure and port != DEFAULT_PORT) or
+ (secure and port != DEFAULT_SECURE_PORT)):
+ hostport += ':' + str(port)
+ # 4.1 12. concatenation of the string "Host:", a U+0020 SPACE
+ # character, and /hostport/, to /fields/.
+ return 'Host: %s\r\n' % hostport
+
+
+# TODO(tyoshino): Define a base class and move these shared methods to that.
+
+
+def receive_bytes(socket, length):
+ received_bytes = []
+ remaining = length
+ while remaining > 0:
+ new_received_bytes = socket.recv(remaining)
+ if not new_received_bytes:
+ raise Exception(
+ 'Connection closed before receiving requested length '
+ '(requested %d bytes but received only %d bytes)' %
+ (length, length - remaining))
+ received_bytes.append(new_received_bytes)
+ remaining -= len(new_received_bytes)
+ return ''.join(received_bytes)
+
+
+# TODO(tyoshino): Now the WebSocketHandshake class diverts these methods. We
+# should move to HTTP parser as specified in RFC 6455. For HyBi 00 and
+# Hixie 75, pack these methods as some parser class.
+
+
+def _read_fields(socket):
+ # 4.1 32. let /fields/ be a list of name-value pairs, initially empty.
+ fields = {}
+ while True:
+ # 4.1 33. let /name/ and /value/ be empty byte arrays
+ name = ''
+ value = ''
+ # 4.1 34. read /name/
+ name = _read_name(socket)
+ if name is None:
+ break
+ # 4.1 35. read spaces
+ # TODO(tyoshino): Skip only one space as described in the spec.
+ ch = _skip_spaces(socket)
+ # 4.1 36. read /value/
+ value = _read_value(socket, ch)
+ # 4.1 37. read a byte from the server
+ ch = receive_bytes(socket, 1)
+ if ch != '\n': # 0x0A
+ raise Exception(
+ 'Expected LF but found %r while reading value %r for header '
+ '%r' % (ch, name, value))
+ # 4.1 38. append an entry to the /fields/ list that has the name
+ # given by the string obtained by interpreting the /name/ byte
+ # array as a UTF-8 stream and the value given by the string
+ # obtained by interpreting the /value/ byte array as a UTF-8 byte
+ # stream.
+ fields.setdefault(name, []).append(value)
+ # 4.1 39. return to the "Field" step above
+ return fields
+
+
+def _read_name(socket):
+ # 4.1 33. let /name/ be empty byte arrays
+ name = ''
+ while True:
+ # 4.1 34. read a byte from the server
+ ch = receive_bytes(socket, 1)
+ if ch == '\r': # 0x0D
+ return None
+ elif ch == '\n': # 0x0A
+ raise Exception(
+ 'Unexpected LF when reading header name %r' % name)
+ elif ch == ':': # 0x3A
+ return name
+ elif ch >= 'A' and ch <= 'Z': # range 0x31 to 0x5A
+ ch = chr(ord(ch) + 0x20)
+ name += ch
+ else:
+ name += ch
+
+
+def _skip_spaces(socket):
+ # 4.1 35. read a byte from the server
+ while True:
+ ch = receive_bytes(socket, 1)
+ if ch == ' ': # 0x20
+ continue
+ return ch
+
+
+def _read_value(socket, ch):
+ # 4.1 33. let /value/ be empty byte arrays
+ value = ''
+ # 4.1 36. read a byte from server.
+ while True:
+ if ch == '\r': # 0x0D
+ return value
+ elif ch == '\n': # 0x0A
+ raise Exception(
+ 'Unexpected LF when reading header value %r' % value)
+ else:
+ value += ch
+ ch = receive_bytes(socket, 1)
+
+
+def read_frame_header(socket):
+ received = receive_bytes(socket, 2)
+
+ first_byte = ord(received[0])
+ fin = (first_byte >> 7) & 1
+ rsv1 = (first_byte >> 6) & 1
+ rsv2 = (first_byte >> 5) & 1
+ rsv3 = (first_byte >> 4) & 1
+ opcode = first_byte & 0xf
+
+ second_byte = ord(received[1])
+ mask = (second_byte >> 7) & 1
+ payload_length = second_byte & 0x7f
+
+ if mask != 0:
+ raise Exception(
+ 'Mask bit must be 0 for frames coming from server')
+
+ if payload_length == 127:
+ extended_payload_length = receive_bytes(socket, 8)
+ payload_length = struct.unpack(
+ '!Q', extended_payload_length)[0]
+ if payload_length > 0x7FFFFFFFFFFFFFFF:
+ raise Exception('Extended payload length >= 2^63')
+ elif payload_length == 126:
+ extended_payload_length = receive_bytes(socket, 2)
+ payload_length = struct.unpack(
+ '!H', extended_payload_length)[0]
+
+ return fin, rsv1, rsv2, rsv3, opcode, payload_length
+
+
+class _TLSSocket(object):
+ """Wrapper for a TLS connection."""
+
+ def __init__(self, raw_socket):
+ self._ssl = socket.ssl(raw_socket)
+
+ def send(self, bytes):
+ return self._ssl.write(bytes)
+
+ def recv(self, size=-1):
+ return self._ssl.read(size)
+
+ def close(self):
+ # Nothing to do.
+ pass
+
+
+class HttpStatusException(Exception):
+ """This exception will be raised when unexpected http status code was
+ received as a result of handshake.
+ """
+
+ def __init__(self, name, status):
+ super(HttpStatusException, self).__init__(name)
+ self.status = status
+
+
+class WebSocketHandshake(object):
+ """Opening handshake processor for the WebSocket protocol (RFC 6455)."""
+
+ def __init__(self, options):
+ self._logger = util.get_class_logger(self)
+
+ self._options = options
+
+ def handshake(self, socket):
+ """Handshake WebSocket.
+
+ Raises:
+ Exception: handshake failed.
+ """
+
+ self._socket = socket
+
+ request_line = _method_line(self._options.resource)
+ self._logger.debug('Opening handshake Request-Line: %r', request_line)
+ self._socket.sendall(request_line)
+
+ fields = []
+ fields.append(_UPGRADE_HEADER)
+ fields.append(_CONNECTION_HEADER)
+
+ fields.append(_format_host_header(
+ self._options.server_host,
+ self._options.server_port,
+ self._options.use_tls))
+
+ if self._options.version is 8:
+ fields.append(_sec_origin_header(self._options.origin))
+ else:
+ fields.append(_origin_header(self._options.origin))
+
+ original_key = os.urandom(16)
+ key = base64.b64encode(original_key)
+ self._logger.debug(
+ 'Sec-WebSocket-Key: %s (%s)', key, util.hexify(original_key))
+ fields.append('Sec-WebSocket-Key: %s\r\n' % key)
+
+ fields.append('Sec-WebSocket-Version: %d\r\n' % self._options.version)
+
+ # Setting up extensions.
+ if len(self._options.extensions) > 0:
+ fields.append('Sec-WebSocket-Extensions: %s\r\n' %
+ ', '.join(self._options.extensions))
+
+ self._logger.debug('Opening handshake request headers: %r', fields)
+
+ for field in fields:
+ self._socket.sendall(field)
+ self._socket.sendall('\r\n')
+
+ self._logger.info('Sent opening handshake request')
+
+ field = ''
+ while True:
+ ch = receive_bytes(self._socket, 1)
+ field += ch
+ if ch == '\n':
+ break
+
+ self._logger.debug('Opening handshake Response-Line: %r', field)
+
+ if len(field) < 7 or not field.endswith('\r\n'):
+ raise Exception('Wrong status line: %r' % field)
+ m = re.match('[^ ]* ([^ ]*) .*', field)
+ if m is None:
+ raise Exception(
+ 'No HTTP status code found in status line: %r' % field)
+ code = m.group(1)
+ if not re.match('[0-9][0-9][0-9]', code):
+ raise Exception(
+ 'HTTP status code %r is not three digit in status line: %r' %
+ (code, field))
+ if code != '101':
+ raise HttpStatusException(
+ 'Expected HTTP status code 101 but found %r in status line: '
+ '%r' % (code, field), int(code))
+ fields = _read_fields(self._socket)
+ ch = receive_bytes(self._socket, 1)
+ if ch != '\n': # 0x0A
+ raise Exception('Expected LF but found: %r' % ch)
+
+ self._logger.debug('Opening handshake response headers: %r', fields)
+
+ # Check /fields/
+ if len(fields['upgrade']) != 1:
+ raise Exception(
+ 'Multiple Upgrade headers found: %s' % fields['upgrade'])
+ if len(fields['connection']) != 1:
+ raise Exception(
+ 'Multiple Connection headers found: %s' % fields['connection'])
+ if fields['upgrade'][0] != 'websocket':
+ raise Exception(
+ 'Unexpected Upgrade header value: %s' % fields['upgrade'][0])
+ if fields['connection'][0].lower() != 'upgrade':
+ raise Exception(
+ 'Unexpected Connection header value: %s' %
+ fields['connection'][0])
+
+ if len(fields['sec-websocket-accept']) != 1:
+ raise Exception(
+ 'Multiple Sec-WebSocket-Accept headers found: %s' %
+ fields['sec-websocket-accept'])
+
+ accept = fields['sec-websocket-accept'][0]
+
+ # Validate
+ try:
+ decoded_accept = base64.b64decode(accept)
+ except TypeError, e:
+ raise HandshakeException(
+ 'Illegal value for header Sec-WebSocket-Accept: ' + accept)
+
+ if len(decoded_accept) != 20:
+ raise HandshakeException(
+ 'Decoded value of Sec-WebSocket-Accept is not 20-byte long')
+
+ self._logger.debug('Actual Sec-WebSocket-Accept: %r (%s)',
+ accept, util.hexify(decoded_accept))
+
+ original_expected_accept = util.sha1_hash(
+ key + WEBSOCKET_ACCEPT_UUID).digest()
+ expected_accept = base64.b64encode(original_expected_accept)
+
+ self._logger.debug('Expected Sec-WebSocket-Accept: %r (%s)',
+ expected_accept,
+ util.hexify(original_expected_accept))
+
+ if accept != expected_accept:
+ raise Exception(
+ 'Invalid Sec-WebSocket-Accept header: %r (expected) != %r '
+ '(actual)' % (accept, expected_accept))
+
+ server_extensions_header = fields.get('sec-websocket-extensions')
+ accepted_extensions = []
+ if server_extensions_header is not None:
+ accepted_extensions = common.parse_extensions(
+ ', '.join(server_extensions_header))
+
+ # Scan accepted extension list to check if there is any unrecognized
+ # extensions or extensions we didn't request in it. Then, for
+ # extensions we request, parse them and store parameters. They will be
+ # used later by each extension.
+ deflate_frame_accepted = False
+ mux_accepted = False
+ for extension in accepted_extensions:
+ if extension.name() == _DEFLATE_FRAME_EXTENSION:
+ if self._options.use_deflate_frame:
+ deflate_frame_accepted = True
+ continue
+ if extension.name() == _MUX_EXTENSION:
+ if self._options.use_mux:
+ mux_accepted = True
+ continue
+ if extension.name() == _PERMESSAGE_DEFLATE_EXTENSION:
+ checker = self._options.check_permessage_deflate
+ if checker:
+ checker(extension)
+ continue
+
+ raise Exception(
+ 'Received unrecognized extension: %s' % extension.name())
+
+ # Let all extensions check the response for extension request.
+
+ if (self._options.use_deflate_frame and
+ not deflate_frame_accepted):
+ raise Exception('%s extension not accepted' %
+ _DEFLATE_FRAME_EXTENSION)
+
+ if self._options.use_mux and not mux_accepted:
+ raise Exception('%s extension not accepted' % _MUX_EXTENSION)
+
+
+class WebSocketHybi00Handshake(object):
+ """Opening handshake processor for the WebSocket protocol version HyBi 00.
+ """
+
+ def __init__(self, options, draft_field):
+ self._logger = util.get_class_logger(self)
+
+ self._options = options
+ self._draft_field = draft_field
+
+ def handshake(self, socket):
+ """Handshake WebSocket.
+
+ Raises:
+ Exception: handshake failed.
+ """
+
+ self._socket = socket
+
+ # 4.1 5. send request line.
+ request_line = _method_line(self._options.resource)
+ self._logger.debug('Opening handshake Request-Line: %r', request_line)
+ self._socket.sendall(request_line)
+ # 4.1 6. Let /fields/ be an empty list of strings.
+ fields = []
+ # 4.1 7. Add the string "Upgrade: WebSocket" to /fields/.
+ fields.append(_UPGRADE_HEADER_HIXIE75)
+ # 4.1 8. Add the string "Connection: Upgrade" to /fields/.
+ fields.append(_CONNECTION_HEADER)
+ # 4.1 9-12. Add Host: field to /fields/.
+ fields.append(_format_host_header(
+ self._options.server_host,
+ self._options.server_port,
+ self._options.use_tls))
+ # 4.1 13. Add Origin: field to /fields/.
+ fields.append(_origin_header(self._options.origin))
+ # TODO: 4.1 14 Add Sec-WebSocket-Protocol: field to /fields/.
+ # TODO: 4.1 15 Add cookie headers to /fields/.
+
+ # 4.1 16-23. Add Sec-WebSocket-Key<n> to /fields/.
+ self._number1, key1 = self._generate_sec_websocket_key()
+ self._logger.debug('Number1: %d', self._number1)
+ fields.append('Sec-WebSocket-Key1: %s\r\n' % key1)
+ self._number2, key2 = self._generate_sec_websocket_key()
+ self._logger.debug('Number2: %d', self._number1)
+ fields.append('Sec-WebSocket-Key2: %s\r\n' % key2)
+
+ fields.append('Sec-WebSocket-Draft: %s\r\n' % self._draft_field)
+
+ # 4.1 24. For each string in /fields/, in a random order: send the
+ # string, encoded as UTF-8, followed by a UTF-8 encoded U+000D CARRIAGE
+ # RETURN U+000A LINE FEED character pair (CRLF).
+ random.shuffle(fields)
+
+ self._logger.debug('Opening handshake request headers: %r', fields)
+ for field in fields:
+ self._socket.sendall(field)
+
+ # 4.1 25. send a UTF-8-encoded U+000D CARRIAGE RETURN U+000A LINE FEED
+ # character pair (CRLF).
+ self._socket.sendall('\r\n')
+ # 4.1 26. let /key3/ be a string consisting of eight random bytes (or
+ # equivalently, a random 64 bit integer encoded in a big-endian order).
+ self._key3 = self._generate_key3()
+ # 4.1 27. send /key3/ to the server.
+ self._socket.sendall(self._key3)
+ self._logger.debug(
+ 'Key3: %r (%s)', self._key3, util.hexify(self._key3))
+
+ self._logger.info('Sent opening handshake request')
+
+ # 4.1 28. Read bytes from the server until either the connection
+ # closes, or a 0x0A byte is read. let /field/ be these bytes, including
+ # the 0x0A bytes.
+ field = ''
+ while True:
+ ch = receive_bytes(self._socket, 1)
+ field += ch
+ if ch == '\n':
+ break
+
+ self._logger.debug('Opening handshake Response-Line: %r', field)
+
+ # if /field/ is not at least seven bytes long, or if the last
+ # two bytes aren't 0x0D and 0x0A respectively, or if it does not
+ # contain at least two 0x20 bytes, then fail the WebSocket connection
+ # and abort these steps.
+ if len(field) < 7 or not field.endswith('\r\n'):
+ raise Exception('Wrong status line: %r' % field)
+ m = re.match('[^ ]* ([^ ]*) .*', field)
+ if m is None:
+ raise Exception('No code found in status line: %r' % field)
+ # 4.1 29. let /code/ be the substring of /field/ that starts from the
+ # byte after the first 0x20 byte, and ends with the byte before the
+ # second 0x20 byte.
+ code = m.group(1)
+ # 4.1 30. if /code/ is not three bytes long, or if any of the bytes in
+ # /code/ are not in the range 0x30 to 0x90, then fail the WebSocket
+ # connection and abort these steps.
+ if not re.match('[0-9][0-9][0-9]', code):
+ raise Exception(
+ 'HTTP status code %r is not three digit in status line: %r' %
+ (code, field))
+ # 4.1 31. if /code/, interpreted as UTF-8, is "101", then move to the
+ # next step.
+ if code != '101':
+ raise HttpStatusException(
+ 'Expected HTTP status code 101 but found %r in status line: '
+ '%r' % (code, field), int(code))
+ # 4.1 32-39. read fields into /fields/
+ fields = _read_fields(self._socket)
+
+ self._logger.debug('Opening handshake response headers: %r', fields)
+
+ # 4.1 40. _Fields processing_
+ # read a byte from server
+ ch = receive_bytes(self._socket, 1)
+ if ch != '\n': # 0x0A
+ raise Exception('Expected LF but found %r' % ch)
+ # 4.1 41. check /fields/
+ if len(fields['upgrade']) != 1:
+ raise Exception(
+ 'Multiple Upgrade headers found: %s' % fields['upgrade'])
+ if len(fields['connection']) != 1:
+ raise Exception(
+ 'Multiple Connection headers found: %s' % fields['connection'])
+ if len(fields['sec-websocket-origin']) != 1:
+ raise Exception(
+ 'Multiple Sec-WebSocket-Origin headers found: %s' %
+ fields['sec-sebsocket-origin'])
+ if len(fields['sec-websocket-location']) != 1:
+ raise Exception(
+ 'Multiple Sec-WebSocket-Location headers found: %s' %
+ fields['sec-sebsocket-location'])
+ # TODO(ukai): protocol
+ # if the entry's name is "upgrade"
+ # if the value is not exactly equal to the string "WebSocket",
+ # then fail the WebSocket connection and abort these steps.
+ if fields['upgrade'][0] != 'WebSocket':
+ raise Exception(
+ 'Unexpected Upgrade header value: %s' % fields['upgrade'][0])
+ # if the entry's name is "connection"
+ # if the value, converted to ASCII lowercase, is not exactly equal
+ # to the string "upgrade", then fail the WebSocket connection and
+ # abort these steps.
+ if fields['connection'][0].lower() != 'upgrade':
+ raise Exception(
+ 'Unexpected Connection header value: %s' %
+ fields['connection'][0])
+ # TODO(ukai): check origin, location, cookie, ..
+
+ # 4.1 42. let /challenge/ be the concatenation of /number_1/,
+ # expressed as a big endian 32 bit integer, /number_2/, expressed
+ # as big endian 32 bit integer, and the eight bytes of /key_3/ in the
+ # order they were sent on the wire.
+ challenge = struct.pack('!I', self._number1)
+ challenge += struct.pack('!I', self._number2)
+ challenge += self._key3
+
+ self._logger.debug(
+ 'Challenge: %r (%s)', challenge, util.hexify(challenge))
+
+ # 4.1 43. let /expected/ be the MD5 fingerprint of /challenge/ as a
+ # big-endian 128 bit string.
+ expected = util.md5_hash(challenge).digest()
+ self._logger.debug(
+ 'Expected challenge response: %r (%s)',
+ expected, util.hexify(expected))
+
+ # 4.1 44. read sixteen bytes from the server.
+ # let /reply/ be those bytes.
+ reply = receive_bytes(self._socket, 16)
+ self._logger.debug(
+ 'Actual challenge response: %r (%s)', reply, util.hexify(reply))
+
+ # 4.1 45. if /reply/ does not exactly equal /expected/, then fail
+ # the WebSocket connection and abort these steps.
+ if expected != reply:
+ raise Exception(
+ 'Bad challenge response: %r (expected) != %r (actual)' %
+ (expected, reply))
+ # 4.1 46. The *WebSocket connection is established*.
+
+ def _generate_sec_websocket_key(self):
+ # 4.1 16. let /spaces_n/ be a random integer from 1 to 12 inclusive.
+ spaces = random.randint(1, 12)
+ # 4.1 17. let /max_n/ be the largest integer not greater than
+ # 4,294,967,295 divided by /spaces_n/.
+ maxnum = 4294967295 / spaces
+ # 4.1 18. let /number_n/ be a random integer from 0 to /max_n/
+ # inclusive.
+ number = random.randint(0, maxnum)
+ # 4.1 19. let /product_n/ be the result of multiplying /number_n/ and
+ # /spaces_n/ together.
+ product = number * spaces
+ # 4.1 20. let /key_n/ be a string consisting of /product_n/, expressed
+ # in base ten using the numerals in the range U+0030 DIGIT ZERO (0) to
+ # U+0039 DIGIT NINE (9).
+ key = str(product)
+ # 4.1 21. insert between one and twelve random characters from the
+ # range U+0021 to U+002F and U+003A to U+007E into /key_n/ at random
+ # positions.
+ available_chars = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1)
+ n = random.randint(1, 12)
+ for _ in xrange(n):
+ ch = random.choice(available_chars)
+ pos = random.randint(0, len(key))
+ key = key[0:pos] + chr(ch) + key[pos:]
+ # 4.1 22. insert /spaces_n/ U+0020 SPACE characters into /key_n/ at
+ # random positions other than start or end of the string.
+ for _ in xrange(spaces):
+ pos = random.randint(1, len(key) - 1)
+ key = key[0:pos] + ' ' + key[pos:]
+ return number, key
+
+ def _generate_key3(self):
+ # 4.1 26. let /key3/ be a string consisting of eight random bytes (or
+ # equivalently, a random 64 bit integer encoded in a big-endian order).
+ return ''.join([chr(random.randint(0, 255)) for _ in xrange(8)])
+
+
+class WebSocketHixie75Handshake(object):
+ """WebSocket handshake processor for IETF Hixie 75."""
+
+ _EXPECTED_RESPONSE = (
+ 'HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
+ _UPGRADE_HEADER_HIXIE75 +
+ _CONNECTION_HEADER)
+
+ def __init__(self, options):
+ self._logger = util.get_class_logger(self)
+
+ self._options = options
+
+ def _skip_headers(self):
+ terminator = '\r\n\r\n'
+ pos = 0
+ while pos < len(terminator):
+ received = receive_bytes(self._socket, 1)
+ if received == terminator[pos]:
+ pos += 1
+ elif received == terminator[0]:
+ pos = 1
+ else:
+ pos = 0
+
+ def handshake(self, socket):
+ self._socket = socket
+
+ request_line = _method_line(self._options.resource)
+ self._logger.debug('Opening handshake Request-Line: %r', request_line)
+ self._socket.sendall(request_line)
+
+ headers = _UPGRADE_HEADER_HIXIE75 + _CONNECTION_HEADER
+ headers += _format_host_header(
+ self._options.server_host,
+ self._options.server_port,
+ self._options.use_tls)
+ headers += _origin_header(self._options.origin)
+ self._logger.debug('Opening handshake request headers: %r', headers)
+ self._socket.sendall(headers)
+
+ self._socket.sendall('\r\n')
+
+ self._logger.info('Sent opening handshake request')
+
+ for expected_char in WebSocketHixie75Handshake._EXPECTED_RESPONSE:
+ received = receive_bytes(self._socket, 1)
+ if expected_char != received:
+ raise Exception('Handshake failure')
+ # We cut corners and skip other headers.
+ self._skip_headers()
+
+
+class WebSocketStream(object):
+ """Frame processor for the WebSocket protocol (RFC 6455)."""
+
+ def __init__(self, socket, handshake):
+ self._handshake = handshake
+ self._socket = socket
+
+ # Filters applied to application data part of data frames.
+ self._outgoing_frame_filter = None
+ self._incoming_frame_filter = None
+
+ if self._handshake._options.use_deflate_frame:
+ self._outgoing_frame_filter = (
+ util._RFC1979Deflater(None, False))
+ self._incoming_frame_filter = util._RFC1979Inflater()
+
+ self._fragmented = False
+
+ def _mask_hybi(self, s):
+ # TODO(tyoshino): os.urandom does open/read/close for every call. If
+ # performance matters, change this to some library call that generates
+ # cryptographically secure pseudo random number sequence.
+ masking_nonce = os.urandom(4)
+ result = [masking_nonce]
+ count = 0
+ for c in s:
+ result.append(chr(ord(c) ^ ord(masking_nonce[count])))
+ count = (count + 1) % len(masking_nonce)
+ return ''.join(result)
+
+ def send_frame_of_arbitrary_bytes(self, header, body):
+ self._socket.sendall(header + self._mask_hybi(body))
+
+ def send_data(self, payload, frame_type, end=True, mask=True,
+ rsv1=0, rsv2=0, rsv3=0):
+ if self._outgoing_frame_filter is not None:
+ payload = self._outgoing_frame_filter.filter(payload)
+
+ if self._fragmented:
+ opcode = OPCODE_CONTINUATION
+ else:
+ opcode = frame_type
+
+ if end:
+ self._fragmented = False
+ fin = 1
+ else:
+ self._fragmented = True
+ fin = 0
+
+ if self._handshake._options.use_deflate_frame:
+ rsv1 = 1
+
+ if mask:
+ mask_bit = 1 << 7
+ else:
+ mask_bit = 0
+
+ header = chr(fin << 7 | rsv1 << 6 | rsv2 << 5 | rsv3 << 4 | opcode)
+ payload_length = len(payload)
+ if payload_length <= 125:
+ header += chr(mask_bit | payload_length)
+ elif payload_length < 1 << 16:
+ header += chr(mask_bit | 126) + struct.pack('!H', payload_length)
+ elif payload_length < 1 << 63:
+ header += chr(mask_bit | 127) + struct.pack('!Q', payload_length)
+ else:
+ raise Exception('Too long payload (%d byte)' % payload_length)
+ if mask:
+ payload = self._mask_hybi(payload)
+ self._socket.sendall(header + payload)
+
+ def send_binary(self, payload, end=True, mask=True):
+ self.send_data(payload, OPCODE_BINARY, end, mask)
+
+ def send_text(self, payload, end=True, mask=True):
+ self.send_data(payload.encode('utf-8'), OPCODE_TEXT, end, mask)
+
+ def _assert_receive_data(self, payload, opcode, fin, rsv1, rsv2, rsv3):
+ (actual_fin, actual_rsv1, actual_rsv2, actual_rsv3, actual_opcode,
+ payload_length) = read_frame_header(self._socket)
+
+ if actual_opcode != opcode:
+ raise Exception(
+ 'Unexpected opcode: %d (expected) vs %d (actual)' %
+ (opcode, actual_opcode))
+
+ if actual_fin != fin:
+ raise Exception(
+ 'Unexpected fin: %d (expected) vs %d (actual)' %
+ (fin, actual_fin))
+
+ if rsv1 is None:
+ rsv1 = 0
+ if self._handshake._options.use_deflate_frame:
+ rsv1 = 1
+
+ if rsv2 is None:
+ rsv2 = 0
+
+ if rsv3 is None:
+ rsv3 = 0
+
+ if actual_rsv1 != rsv1:
+ raise Exception(
+ 'Unexpected rsv1: %r (expected) vs %r (actual)' %
+ (rsv1, actual_rsv1))
+
+ if actual_rsv2 != rsv2:
+ raise Exception(
+ 'Unexpected rsv2: %r (expected) vs %r (actual)' %
+ (rsv2, actual_rsv2))
+
+ if actual_rsv3 != rsv3:
+ raise Exception(
+ 'Unexpected rsv3: %r (expected) vs %r (actual)' %
+ (rsv3, actual_rsv3))
+
+ received = receive_bytes(self._socket, payload_length)
+
+ if self._incoming_frame_filter is not None:
+ received = self._incoming_frame_filter.filter(received)
+
+ if len(received) != len(payload):
+ raise Exception(
+ 'Unexpected payload length: %d (expected) vs %d (actual)' %
+ (len(payload), len(received)))
+
+ if payload != received:
+ raise Exception(
+ 'Unexpected payload: %r (expected) vs %r (actual)' %
+ (payload, received))
+
+ def assert_receive_binary(self, payload, opcode=OPCODE_BINARY, fin=1,
+ rsv1=None, rsv2=None, rsv3=None):
+ self._assert_receive_data(payload, opcode, fin, rsv1, rsv2, rsv3)
+
+ def assert_receive_text(self, payload, opcode=OPCODE_TEXT, fin=1,
+ rsv1=None, rsv2=None, rsv3=None):
+ self._assert_receive_data(payload.encode('utf-8'), opcode, fin, rsv1,
+ rsv2, rsv3)
+
+ def _build_close_frame(self, code, reason, mask):
+ frame = chr(1 << 7 | OPCODE_CLOSE)
+
+ if code is not None:
+ body = struct.pack('!H', code) + reason.encode('utf-8')
+ else:
+ body = ''
+ if mask:
+ frame += chr(1 << 7 | len(body)) + self._mask_hybi(body)
+ else:
+ frame += chr(len(body)) + body
+ return frame
+
+ def send_close(self, code, reason):
+ self._socket.sendall(
+ self._build_close_frame(code, reason, True))
+
+ def assert_receive_close(self, code, reason):
+ expected_frame = self._build_close_frame(code, reason, False)
+ actual_frame = receive_bytes(self._socket, len(expected_frame))
+ if actual_frame != expected_frame:
+ raise Exception(
+ 'Unexpected close frame: %r (expected) vs %r (actual)' %
+ (expected_frame, actual_frame))
+
+
+class WebSocketStreamHixie75(object):
+ """Frame processor for the WebSocket protocol version Hixie 75 and HyBi 00.
+ """
+
+ _CLOSE_FRAME = '\xff\x00'
+
+ def __init__(self, socket, unused_handshake):
+ self._socket = socket
+
+ def send_frame_of_arbitrary_bytes(self, header, body):
+ self._socket.sendall(header + body)
+
+ def send_data(self, payload, unused_frame_typem, unused_end, unused_mask):
+ frame = ''.join(['\x00', payload, '\xff'])
+ self._socket.sendall(frame)
+
+ def send_binary(self, unused_payload, unused_end, unused_mask):
+ pass
+
+ def send_text(self, payload, unused_end, unused_mask):
+ encoded_payload = payload.encode('utf-8')
+ frame = ''.join(['\x00', encoded_payload, '\xff'])
+ self._socket.sendall(frame)
+
+ def assert_receive_binary(self, payload, opcode=OPCODE_BINARY, fin=1,
+ rsv1=0, rsv2=0, rsv3=0):
+ raise Exception('Binary frame is not supported in hixie75')
+
+ def assert_receive_text(self, payload):
+ received = receive_bytes(self._socket, 1)
+
+ if received != '\x00':
+ raise Exception(
+ 'Unexpected frame type: %d (expected) vs %d (actual)' %
+ (0, ord(received)))
+
+ received = receive_bytes(self._socket, len(payload) + 1)
+ if received[-1] != '\xff':
+ raise Exception(
+ 'Termination expected: 0xff (expected) vs %r (actual)' %
+ received)
+
+ if received[0:-1] != payload:
+ raise Exception(
+ 'Unexpected payload: %r (expected) vs %r (actual)' %
+ (payload, received[0:-1]))
+
+ def send_close(self, code, reason):
+ self._socket.sendall(self._CLOSE_FRAME)
+
+ def assert_receive_close(self, unused_code, unused_reason):
+ closing = receive_bytes(self._socket, len(self._CLOSE_FRAME))
+ if closing != self._CLOSE_FRAME:
+ raise Exception('Didn\'t receive closing handshake')
+
+
+class ClientOptions(object):
+ """Holds option values to configure the Client object."""
+
+ def __init__(self):
+ self.version = 13
+ self.server_host = ''
+ self.origin = ''
+ self.resource = ''
+ self.server_port = -1
+ self.socket_timeout = 1000
+ self.use_tls = False
+ self.extensions = []
+ # Enable deflate-application-data.
+ self.use_deflate_frame = False
+ # Enable mux
+ self.use_mux = False
+
+ def enable_deflate_frame(self):
+ self.use_deflate_frame = True
+ self.extensions.append(_DEFLATE_FRAME_EXTENSION)
+
+ def enable_mux(self):
+ self.use_mux = True
+ self.extensions.append(_MUX_EXTENSION)
+
+
+def connect_socket_with_retry(host, port, timeout, use_tls,
+ retry=10, sleep_sec=0.1):
+ retry_count = 0
+ while retry_count < retry:
+ try:
+ s = socket.socket()
+ s.settimeout(timeout)
+ s.connect((host, port))
+ if use_tls:
+ return _TLSSocket(s)
+ return s
+ except socket.error, e:
+ if e.errno != errno.ECONNREFUSED:
+ raise
+ else:
+ retry_count = retry_count + 1
+ time.sleep(sleep_sec)
+
+ return None
+
+
+class Client(object):
+ """WebSocket client."""
+
+ def __init__(self, options, handshake, stream_class):
+ self._logger = util.get_class_logger(self)
+
+ self._options = options
+ self._socket = None
+
+ self._handshake = handshake
+ self._stream_class = stream_class
+
+ def connect(self):
+ self._socket = connect_socket_with_retry(
+ self._options.server_host,
+ self._options.server_port,
+ self._options.socket_timeout,
+ self._options.use_tls)
+
+ self._handshake.handshake(self._socket)
+
+ self._stream = self._stream_class(self._socket, self._handshake)
+
+ self._logger.info('Connection established')
+
+ def send_frame_of_arbitrary_bytes(self, header, body):
+ self._stream.send_frame_of_arbitrary_bytes(header, body)
+
+ def send_message(self, message, end=True, binary=False, raw=False,
+ mask=True):
+ if binary:
+ self._stream.send_binary(message, end, mask)
+ elif raw:
+ self._stream.send_data(message, OPCODE_TEXT, end, mask)
+ else:
+ self._stream.send_text(message, end, mask)
+
+ def assert_receive(self, payload, binary=False):
+ if binary:
+ self._stream.assert_receive_binary(payload)
+ else:
+ self._stream.assert_receive_text(payload)
+
+ def send_close(self, code=STATUS_NORMAL_CLOSURE, reason=''):
+ self._stream.send_close(code, reason)
+
+ def assert_receive_close(self, code=STATUS_NORMAL_CLOSURE, reason=''):
+ self._stream.assert_receive_close(code, reason)
+
+ def close_socket(self):
+ self._socket.close()
+
+ def assert_connection_closed(self):
+ try:
+ read_data = receive_bytes(self._socket, 1)
+ except Exception, e:
+ if str(e).find(
+ 'Connection closed before receiving requested length ') == 0:
+ return
+ try:
+ error_number, message = e
+ for error_name in ['ECONNRESET', 'WSAECONNRESET']:
+ if (error_name in dir(errno) and
+ error_number == getattr(errno, error_name)):
+ return
+ except:
+ raise e
+ raise e
+
+ raise Exception('Connection is not closed (Read: %r)' % read_data)
+
+
+def create_client(options):
+ return Client(
+ options, WebSocketHandshake(options), WebSocketStream)
+
+
+def create_client_hybi00(options):
+ return Client(
+ options,
+ WebSocketHybi00Handshake(options, '0'),
+ WebSocketStreamHixie75)
+
+
+def create_client_hixie75(options):
+ return Client(
+ options, WebSocketHixie75Handshake(options), WebSocketStreamHixie75)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/endtoend_with_external_server.py b/testing/web-platform/tests/tools/pywebsocket/src/test/endtoend_with_external_server.py
new file mode 100755
index 000000000..47f86fdb4
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/endtoend_with_external_server.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Test for end-to-end with external server.
+
+This test is not run by run_all.py because it requires some preparations.
+If you would like to run this test correctly, launch Apache with mod_python
+and mod_pywebsocket manually. In addition, you should pass allow_draft75 option
+and example path as handler_scan option and Apache's DocumentRoot.
+"""
+
+
+import optparse
+import sys
+import test.test_endtoend
+import unittest
+
+
+_DEFAULT_WEB_SOCKET_PORT = 80
+
+
+class EndToEndTestWithExternalServer(test.test_endtoend.EndToEndTest):
+ pass
+
+if __name__ == '__main__':
+ parser = optparse.OptionParser()
+ parser.add_option('-p', '--port', dest='port', type='int',
+ default=_DEFAULT_WEB_SOCKET_PORT,
+ help='external test server port.')
+ (options, args) = parser.parse_args()
+
+ test.test_endtoend._use_external_server = True
+ test.test_endtoend._external_server_port = options.port
+
+ unittest.main(argv=[sys.argv[0]])
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/mock.py b/testing/web-platform/tests/tools/pywebsocket/src/test/mock.py
new file mode 100644
index 000000000..6bffcac48
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/mock.py
@@ -0,0 +1,221 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Mocks for testing.
+"""
+
+
+import Queue
+import threading
+
+from mod_pywebsocket import common
+from mod_pywebsocket.stream import StreamHixie75
+
+
+class _MockConnBase(object):
+ """Base class of mocks for mod_python.apache.mp_conn.
+
+ This enables tests to check what is written to a (mock) mp_conn.
+ """
+
+ def __init__(self):
+ self._write_data = []
+ self.remote_addr = 'fake_address'
+
+ def write(self, data):
+ """Override mod_python.apache.mp_conn.write."""
+
+ self._write_data.append(data)
+
+ def written_data(self):
+ """Get bytes written to this mock."""
+
+ return ''.join(self._write_data)
+
+
+class MockConn(_MockConnBase):
+ """Mock for mod_python.apache.mp_conn.
+
+ This enables tests to specify what should be read from a (mock) mp_conn as
+ well as to check what is written to it.
+ """
+
+ def __init__(self, read_data):
+ """Constructs an instance.
+
+ Args:
+ read_data: bytes that should be returned when read* methods are
+ called.
+ """
+
+ _MockConnBase.__init__(self)
+ self._read_data = read_data
+ self._read_pos = 0
+
+ def readline(self):
+ """Override mod_python.apache.mp_conn.readline."""
+
+ if self._read_pos >= len(self._read_data):
+ return ''
+ end_index = self._read_data.find('\n', self._read_pos) + 1
+ if not end_index:
+ end_index = len(self._read_data)
+ return self._read_up_to(end_index)
+
+ def read(self, length):
+ """Override mod_python.apache.mp_conn.read."""
+
+ if self._read_pos >= len(self._read_data):
+ return ''
+ end_index = min(len(self._read_data), self._read_pos + length)
+ return self._read_up_to(end_index)
+
+ def _read_up_to(self, end_index):
+ line = self._read_data[self._read_pos:end_index]
+ self._read_pos = end_index
+ return line
+
+
+class MockBlockingConn(_MockConnBase):
+ """Blocking mock for mod_python.apache.mp_conn.
+
+ This enables tests to specify what should be read from a (mock) mp_conn as
+ well as to check what is written to it.
+ Callers of read* methods will block if there is no bytes available.
+ """
+
+ def __init__(self):
+ _MockConnBase.__init__(self)
+ self._queue = Queue.Queue()
+
+ def readline(self):
+ """Override mod_python.apache.mp_conn.readline."""
+ line = ''
+ while True:
+ c = self._queue.get()
+ line += c
+ if c == '\n':
+ return line
+
+ def read(self, length):
+ """Override mod_python.apache.mp_conn.read."""
+
+ data = ''
+ for unused in range(length):
+ data += self._queue.get()
+ return data
+
+ def put_bytes(self, bytes):
+ """Put bytes to be read from this mock.
+
+ Args:
+ bytes: bytes to be read.
+ """
+
+ for byte in bytes:
+ self._queue.put(byte)
+
+
+class MockTable(dict):
+ """Mock table.
+
+ This mimics mod_python mp_table. Note that only the methods used by
+ tests are overridden.
+ """
+
+ def __init__(self, copy_from={}):
+ if isinstance(copy_from, dict):
+ copy_from = copy_from.items()
+ for key, value in copy_from:
+ self.__setitem__(key, value)
+
+ def __getitem__(self, key):
+ return super(MockTable, self).__getitem__(key.lower())
+
+ def __setitem__(self, key, value):
+ super(MockTable, self).__setitem__(key.lower(), value)
+
+ def get(self, key, def_value=None):
+ return super(MockTable, self).get(key.lower(), def_value)
+
+
+class MockRequest(object):
+ """Mock request.
+
+ This mimics mod_python request.
+ """
+
+ def __init__(self, uri=None, headers_in={}, connection=None, method='GET',
+ protocol='HTTP/1.1', is_https=False):
+ """Construct an instance.
+
+ Arguments:
+ uri: URI of the request.
+ headers_in: Request headers.
+ connection: Connection used for the request.
+ method: request method.
+ is_https: Whether this request is over SSL.
+
+ See the document of mod_python Request for details.
+ """
+ self.uri = uri
+ self.unparsed_uri = uri
+ self.connection = connection
+ self.method = method
+ self.protocol = protocol
+ self.headers_in = MockTable(headers_in)
+ # self.is_https_ needs to be accessible from tests. To avoid name
+ # conflict with self.is_https(), it is named as such.
+ self.is_https_ = is_https
+ self.ws_stream = StreamHixie75(self, True)
+ self.ws_close_code = None
+ self.ws_close_reason = None
+ self.ws_version = common.VERSION_HYBI00
+ self.ws_deflate = False
+
+ def is_https(self):
+ """Return whether this request is over SSL."""
+ return self.is_https_
+
+
+class MockDispatcher(object):
+ """Mock for dispatch.Dispatcher."""
+
+ def __init__(self):
+ self.do_extra_handshake_called = False
+
+ def do_extra_handshake(self, conn_context):
+ self.do_extra_handshake_called = True
+
+ def transfer_data(self, conn_context):
+ pass
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/mux_client_for_testing.py b/testing/web-platform/tests/tools/pywebsocket/src/test/mux_client_for_testing.py
new file mode 100644
index 000000000..dd5435a8c
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/mux_client_for_testing.py
@@ -0,0 +1,690 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""WebSocket client utility for testing mux extension.
+
+This code should be independent from mod_pywebsocket. See the comment of
+client_for_testing.py.
+
+NOTE: This code is far from robust like client_for_testing.py.
+"""
+
+
+
+import Queue
+import base64
+import collections
+import email
+import email.parser
+import logging
+import math
+import os
+import random
+import socket
+import struct
+import threading
+
+from mod_pywebsocket import util
+
+from test import client_for_testing
+
+
+_CONTROL_CHANNEL_ID = 0
+_DEFAULT_CHANNEL_ID = 1
+
+_MUX_OPCODE_ADD_CHANNEL_REQUEST = 0
+_MUX_OPCODE_ADD_CHANNEL_RESPONSE = 1
+_MUX_OPCODE_FLOW_CONTROL = 2
+_MUX_OPCODE_DROP_CHANNEL = 3
+_MUX_OPCODE_NEW_CHANNEL_SLOT = 4
+
+
+class _ControlBlock:
+ def __init__(self, opcode):
+ self.opcode = opcode
+
+
+def _parse_handshake_response(response):
+ status_line, header_lines = response.split('\r\n', 1)
+
+ words = status_line.split(' ')
+ if len(words) < 3:
+ raise ValueError('Bad Status-Line syntax %r' % status_line)
+ [version, response_code] = words[:2]
+ if version != 'HTTP/1.1':
+ raise ValueError('Bad response version %r' % version)
+
+ if response_code != '101':
+ raise ValueError('Bad response code %r ' % response_code)
+ headers = email.parser.Parser().parsestr(header_lines)
+ return headers
+
+
+def _parse_channel_id(data, offset=0):
+ length = len(data)
+ remaining = length - offset
+
+ if remaining <= 0:
+ raise Exception('No channel id found')
+
+ channel_id = ord(data[offset])
+ channel_id_length = 1
+ if channel_id & 0xe0 == 0xe0:
+ if remaining < 4:
+ raise Exception('Invalid channel id format')
+ channel_id = struct.unpack('!L',
+ data[offset:offset+4])[0] & 0x1fffffff
+ channel_id_length = 4
+ elif channel_id & 0xc0 == 0xc0:
+ if remaining < 3:
+ raise Exception('Invalid channel id format')
+ channel_id = (((channel_id & 0x1f) << 16) +
+ struct.unpack('!H', data[offset+1:offset+3])[0])
+ channel_id_length = 3
+ elif channel_id & 0x80 == 0x80:
+ if remaining < 2:
+ raise Exception('Invalid channel id format')
+ channel_id = struct.unpack('!H', data[offset:offset+2])[0] & 0x3fff
+ channel_id_length = 2
+
+ return channel_id, channel_id_length
+
+
+def _parse_number(data, offset=0):
+ first_byte = ord(data[offset])
+ if (first_byte & 0x80) != 0:
+ raise Exception('The MSB of number field must be unset')
+ first_byte = first_byte & 0x7f
+ if first_byte == 127:
+ if offset + 9 > len(data):
+ raise Exception('Invalid number')
+ return struct.unpack('!Q', data[offset+1:offset+9])[0], 9
+ if first_byte == 126:
+ if offset + 3 > len(data):
+ raise Exception('Invalid number')
+ return struct.unpack('!H', data[offset+1:offset+3])[0], 3
+ return first_byte, 1
+
+
+def _parse_size_and_contents(data, offset=0):
+ size, advance = _parse_number(data, offset)
+ start_position = offset + advance
+ end_position = start_position + size
+ if len(data) < end_position:
+ raise Exception('Invalid size of control block (%d < %d)' % (
+ len(data), end_position))
+ return data[start_position:end_position], size + advance
+
+
+def _parse_control_blocks(data):
+ blocks = []
+ length = len(data)
+ pos = 0
+
+ while pos < length:
+ first_byte = ord(data[pos])
+ pos += 1
+ opcode = (first_byte >> 5) & 0x7
+ block = _ControlBlock(opcode)
+
+ # TODO(bashi): Support more opcode
+ if opcode == _MUX_OPCODE_ADD_CHANNEL_RESPONSE:
+ block.encode = first_byte & 3
+ block.rejected = (first_byte >> 4) & 1
+
+ channel_id, advance = _parse_channel_id(data, pos)
+ block.channel_id = channel_id
+ pos += advance
+
+ encoded_handshake, advance = _parse_size_and_contents(data, pos)
+ block.encoded_handshake = encoded_handshake
+ pos += advance
+ blocks.append(block)
+ elif opcode == _MUX_OPCODE_DROP_CHANNEL:
+ block.mux_error = (first_byte >> 4) & 1
+
+ channel_id, advance = _parse_channel_id(data, pos)
+ block.channel_id = channel_id
+ pos += advance
+
+ reason, advance = _parse_size_and_contents(data, pos)
+ if len(reason) == 0:
+ block.drop_code = None
+ block.drop_message = ''
+ elif len(reason) >= 2:
+ block.drop_code = struct.unpack('!H', reason[:2])[0]
+ block.drop_message = reason[2:]
+ else:
+ raise Exception('Invalid DropChannel')
+ pos += advance
+ blocks.append(block)
+ elif opcode == _MUX_OPCODE_FLOW_CONTROL:
+ channel_id, advance = _parse_channel_id(data, pos)
+ block.channel_id = channel_id
+ pos += advance
+ send_quota, advance = _parse_number(data, pos)
+ block.send_quota = send_quota
+ pos += advance
+ blocks.append(block)
+ elif opcode == _MUX_OPCODE_NEW_CHANNEL_SLOT:
+ fallback = first_byte & 1
+ slots, advance = _parse_number(data, pos)
+ pos += advance
+ send_quota, advance = _parse_number(data, pos)
+ pos += advance
+ if fallback == 1 and (slots != 0 or send_quota != 0):
+ raise Exception('slots and send_quota must be zero if F bit '
+ 'is set')
+ block.fallback = fallback
+ block.slots = slots
+ block.send_quota = send_quota
+ blocks.append(block)
+ else:
+ raise Exception(
+ 'Unsupported mux opcode %d received' % opcode)
+
+ return blocks
+
+
+def _encode_channel_id(channel_id):
+ if channel_id < 0:
+ raise ValueError('Channel id %d must not be negative' % channel_id)
+
+ if channel_id < 2 ** 7:
+ return chr(channel_id)
+ if channel_id < 2 ** 14:
+ return struct.pack('!H', 0x8000 + channel_id)
+ if channel_id < 2 ** 21:
+ first = chr(0xc0 + (channel_id >> 16))
+ return first + struct.pack('!H', channel_id & 0xffff)
+ if channel_id < 2 ** 29:
+ return struct.pack('!L', 0xe0000000 + channel_id)
+
+ raise ValueError('Channel id %d is too large' % channel_id)
+
+
+def _encode_number(number):
+ if number <= 125:
+ return chr(number)
+ elif number < (1 << 16):
+ return chr(0x7e) + struct.pack('!H', number)
+ elif number < (1 << 63):
+ return chr(0x7f) + struct.pack('!Q', number)
+ else:
+ raise Exception('Invalid number')
+
+
+def _create_add_channel_request(channel_id, encoded_handshake,
+ encoding=0):
+ length = len(encoded_handshake)
+ handshake_length = _encode_number(length)
+
+ first_byte = (_MUX_OPCODE_ADD_CHANNEL_REQUEST << 5) | encoding
+ return (chr(first_byte) + _encode_channel_id(channel_id) +
+ handshake_length + encoded_handshake)
+
+
+def _create_flow_control(channel_id, replenished_quota):
+ first_byte = (_MUX_OPCODE_FLOW_CONTROL << 5)
+ return (chr(first_byte) + _encode_channel_id(channel_id) +
+ _encode_number(replenished_quota))
+
+
+class _MuxReaderThread(threading.Thread):
+ """Mux reader thread.
+
+ Reads frames and passes them to the mux client. This thread accesses
+ private functions/variables of the mux client.
+ """
+
+ def __init__(self, mux):
+ threading.Thread.__init__(self)
+ self.setDaemon(True)
+ self._mux = mux
+ self._stop_requested = False
+
+ def _receive_message(self):
+ first_opcode = None
+ pending_payload = []
+ while not self._stop_requested:
+ fin, rsv1, rsv2, rsv3, opcode, payload_length = (
+ client_for_testing.read_frame_header(self._mux._socket))
+
+ if not first_opcode:
+ if opcode == client_for_testing.OPCODE_TEXT:
+ raise Exception('Received a text message on physical '
+ 'connection')
+ if opcode == client_for_testing.OPCODE_CONTINUATION:
+ raise Exception('Received an intermediate frame but '
+ 'fragmentation was not started')
+ if (opcode == client_for_testing.OPCODE_BINARY or
+ opcode == client_for_testing.OPCODE_PONG or
+ opcode == client_for_testing.OPCODE_PONG or
+ opcode == client_for_testing.OPCODE_CLOSE):
+ first_opcode = opcode
+ else:
+ raise Exception('Received an undefined opcode frame: %d' %
+ opcode)
+
+ elif opcode != client_for_testing.OPCODE_CONTINUATION:
+ raise Exception('Received a new opcode before '
+ 'terminating fragmentation')
+
+ payload = client_for_testing.receive_bytes(
+ self._mux._socket, payload_length)
+
+ if self._mux._incoming_frame_filter is not None:
+ payload = self._mux._incoming_frame_filter.filter(payload)
+
+ pending_payload.append(payload)
+
+ if fin:
+ break
+
+ if self._stop_requested:
+ return None, None
+
+ message = ''.join(pending_payload)
+ return first_opcode, message
+
+ def request_stop(self):
+ self._stop_requested = True
+
+ def run(self):
+ try:
+ while not self._stop_requested:
+ # opcode is OPCODE_BINARY or control opcodes when a message
+ # is succesfully received.
+ opcode, message = self._receive_message()
+ if not opcode:
+ return
+ if opcode == client_for_testing.OPCODE_BINARY:
+ channel_id, advance = _parse_channel_id(message)
+ self._mux._dispatch_frame(channel_id, message[advance:])
+ else:
+ self._mux._process_control_message(opcode, message)
+ finally:
+ self._mux._notify_reader_done()
+
+
+class _InnerFrame(object):
+ def __init__(self, fin, rsv1, rsv2, rsv3, opcode, payload):
+ self.fin = fin
+ self.rsv1 = rsv1
+ self.rsv2 = rsv2
+ self.rsv3 = rsv3
+ self.opcode = opcode
+ self.payload = payload
+
+
+class _LogicalChannelData(object):
+ def __init__(self):
+ self.queue = Queue.Queue()
+ self.send_quota = 0
+ self.receive_quota = 0
+
+
+class MuxClient(object):
+ """WebSocket mux client.
+
+ Note that this class is NOT thread-safe. Do not access an instance of this
+ class from multiple threads at a same time.
+ """
+
+ def __init__(self, options):
+ self._logger = util.get_class_logger(self)
+
+ self._options = options
+ self._options.enable_mux()
+ self._stream = None
+ self._socket = None
+ self._handshake = client_for_testing.WebSocketHandshake(self._options)
+ self._incoming_frame_filter = None
+ self._outgoing_frame_filter = None
+
+ self._is_active = False
+ self._read_thread = None
+ self._control_blocks_condition = threading.Condition()
+ self._control_blocks = []
+ self._channel_slots = collections.deque()
+ self._logical_channels_condition = threading.Condition();
+ self._logical_channels = {}
+ self._timeout = 2
+ self._physical_connection_close_event = None
+ self._physical_connection_close_message = None
+
+ def _parse_inner_frame(self, data):
+ if len(data) == 0:
+ raise Exception('Invalid encapsulated frame received')
+
+ first_byte = ord(data[0])
+ fin = (first_byte << 7) & 1
+ rsv1 = (first_byte << 6) & 1
+ rsv2 = (first_byte << 5) & 1
+ rsv3 = (first_byte << 4) & 1
+ opcode = first_byte & 0xf
+
+ if self._outgoing_frame_filter:
+ payload = self._outgoing_frame_filter.filter(
+ data[1:])
+ else:
+ payload = data[1:]
+
+ return _InnerFrame(fin, rsv1, rsv2, rsv3, opcode, payload)
+
+ def _process_mux_control_blocks(self):
+ for block in self._control_blocks:
+ if block.opcode == _MUX_OPCODE_ADD_CHANNEL_RESPONSE:
+ # AddChannelResponse will be handled in add_channel().
+ continue
+ elif block.opcode == _MUX_OPCODE_FLOW_CONTROL:
+ try:
+ self._logical_channels_condition.acquire()
+ if not block.channel_id in self._logical_channels:
+ raise Exception('Invalid flow control received for '
+ 'channel id %d' % block.channel_id)
+ self._logical_channels[block.channel_id].send_quota += (
+ block.send_quota)
+ self._logical_channels_condition.notify()
+ finally:
+ self._logical_channels_condition.release()
+ elif block.opcode == _MUX_OPCODE_NEW_CHANNEL_SLOT:
+ self._channel_slots.extend([block.send_quota] * block.slots)
+
+ def _dispatch_frame(self, channel_id, payload):
+ if channel_id == _CONTROL_CHANNEL_ID:
+ try:
+ self._control_blocks_condition.acquire()
+ self._control_blocks += _parse_control_blocks(payload)
+ self._process_mux_control_blocks()
+ self._control_blocks_condition.notify()
+ finally:
+ self._control_blocks_condition.release()
+ else:
+ try:
+ self._logical_channels_condition.acquire()
+ if not channel_id in self._logical_channels:
+ raise Exception('Received logical frame on channel id '
+ '%d, which is not established' %
+ channel_id)
+
+ inner_frame = self._parse_inner_frame(payload)
+ self._logical_channels[channel_id].receive_quota -= (
+ len(inner_frame.payload))
+ if self._logical_channels[channel_id].receive_quota < 0:
+ raise Exception('The server violates quota on '
+ 'channel id %d' % channel_id)
+ finally:
+ self._logical_channels_condition.release()
+ self._logical_channels[channel_id].queue.put(inner_frame)
+
+ def _process_control_message(self, opcode, message):
+ # Ping/Pong are not supported.
+ if opcode == client_for_testing.OPCODE_CLOSE:
+ self._physical_connection_close_message = message
+ if self._is_active:
+ self._stream.send_close(
+ code=client_for_testing.STATUS_NORMAL_CLOSURE, reason='')
+ self._read_thread.request_stop()
+
+ if self._physical_connection_close_event:
+ self._physical_connection_close_event.set()
+
+ def _notify_reader_done(self):
+ self._logger.debug('Read thread terminated.')
+ self.close_socket()
+
+ def _assert_channel_slot_available(self):
+ try:
+ self._control_blocks_condition.acquire()
+ if len(self._channel_slots) == 0:
+ # Wait once
+ self._control_blocks_condition.wait(timeout=self._timeout)
+ finally:
+ self._control_blocks_condition.release()
+
+ if len(self._channel_slots) == 0:
+ raise Exception('Failed to receive NewChannelSlot')
+
+ def _assert_send_quota_available(self, channel_id):
+ try:
+ self._logical_channels_condition.acquire()
+ if self._logical_channels[channel_id].send_quota == 0:
+ # Wait once
+ self._logical_channels_condition.wait(timeout=self._timeout)
+ finally:
+ self._logical_channels_condition.release()
+
+ if self._logical_channels[channel_id].send_quota == 0:
+ raise Exception('Failed to receive FlowControl for channel id %d' %
+ channel_id)
+
+ def connect(self):
+ self._socket = client_for_testing.connect_socket_with_retry(
+ self._options.server_host,
+ self._options.server_port,
+ self._options.socket_timeout,
+ self._options.use_tls)
+
+ self._handshake.handshake(self._socket)
+ self._stream = client_for_testing.WebSocketStream(
+ self._socket, self._handshake)
+
+ self._logical_channels[_DEFAULT_CHANNEL_ID] = _LogicalChannelData()
+
+ self._read_thread = _MuxReaderThread(self)
+ self._read_thread.start()
+
+ self._assert_channel_slot_available()
+ self._assert_send_quota_available(_DEFAULT_CHANNEL_ID)
+
+ self._is_active = True
+ self._logger.info('Connection established')
+
+ def add_channel(self, channel_id, options):
+ if not self._is_active:
+ raise Exception('Mux client is not active')
+
+ if channel_id in self._logical_channels:
+ raise Exception('Channel id %d already exists' % channel_id)
+
+ try:
+ send_quota = self._channel_slots.popleft()
+ except IndexError, e:
+ raise Exception('No channel slots: %r' % e)
+
+ # Create AddChannel request
+ request_line = 'GET %s HTTP/1.1\r\n' % options.resource
+ fields = []
+ if options.server_port == client_for_testing.DEFAULT_PORT:
+ fields.append('Host: %s\r\n' % options.server_host.lower())
+ else:
+ fields.append('Host: %s:%d\r\n' % (options.server_host.lower(),
+ options.server_port))
+ fields.append('Origin: %s\r\n' % options.origin.lower())
+ fields.append('Connection: Upgrade\r\n')
+
+ if len(options.extensions) > 0:
+ fields.append('Sec-WebSocket-Extensions: %s\r\n' %
+ ', '.join(options.extensions))
+
+ handshake = request_line + ''.join(fields) + '\r\n'
+ add_channel_request = _create_add_channel_request(
+ channel_id, handshake)
+ payload = _encode_channel_id(_CONTROL_CHANNEL_ID) + add_channel_request
+ self._stream.send_binary(payload)
+
+ # Wait AddChannelResponse
+ self._logger.debug('Waiting AddChannelResponse for the request...')
+ response = None
+ try:
+ self._control_blocks_condition.acquire()
+ while True:
+ for block in self._control_blocks:
+ if block.opcode != _MUX_OPCODE_ADD_CHANNEL_RESPONSE:
+ continue
+ if block.channel_id == channel_id:
+ response = block
+ self._control_blocks.remove(response)
+ break
+ if response:
+ break
+ self._control_blocks_condition.wait(self._timeout)
+ if not self._is_active:
+ raise Exception('AddChannelRequest timed out')
+ finally:
+ self._control_blocks_condition.release()
+
+ # Validate AddChannelResponse
+ if response.rejected:
+ raise Exception('The server rejected AddChannelRequest')
+
+ fields = _parse_handshake_response(response.encoded_handshake)
+
+ # Should we reject when Upgrade, Connection, or Sec-WebSocket-Accept
+ # headers exist?
+
+ self._logical_channels_condition.acquire()
+ self._logical_channels[channel_id] = _LogicalChannelData()
+ self._logical_channels[channel_id].send_quota = send_quota
+ self._logical_channels_condition.release()
+
+ self._logger.debug('Logical channel %d established' % channel_id)
+
+ def _check_logical_channel_is_opened(self, channel_id):
+ if not self._is_active:
+ raise Exception('Mux client is not active')
+
+ if not channel_id in self._logical_channels:
+ raise Exception('Logical channel %d is not established.')
+
+ def drop_channel(self, channel_id):
+ # TODO(bashi): Implement
+ pass
+
+ def send_flow_control(self, channel_id, replenished_quota):
+ self._check_logical_channel_is_opened(channel_id)
+ flow_control = _create_flow_control(channel_id, replenished_quota)
+ payload = _encode_channel_id(_CONTROL_CHANNEL_ID) + flow_control
+ # Replenish receive quota
+ try:
+ self._logical_channels_condition.acquire()
+ self._logical_channels[channel_id].receive_quota += (
+ replenished_quota)
+ finally:
+ self._logical_channels_condition.release()
+ self._stream.send_binary(payload)
+
+ def send_message(self, channel_id, message, end=True, binary=False):
+ self._check_logical_channel_is_opened(channel_id)
+
+ if binary:
+ first_byte = (end << 7) | client_for_testing.OPCODE_BINARY
+ else:
+ first_byte = (end << 7) | client_for_testing.OPCODE_TEXT
+ message = message.encode('utf-8')
+
+ try:
+ self._logical_channels_condition.acquire()
+ if self._logical_channels[channel_id].send_quota < len(message):
+ raise Exception('Send quota violation: %d < %d' % (
+ self._logical_channels[channel_id].send_quota,
+ len(message)))
+
+ self._logical_channels[channel_id].send_quota -= len(message)
+ finally:
+ self._logical_channels_condition.release()
+ payload = _encode_channel_id(channel_id) + chr(first_byte) + message
+ self._stream.send_binary(payload)
+
+ def assert_receive(self, channel_id, payload, binary=False):
+ self._check_logical_channel_is_opened(channel_id)
+
+ try:
+ inner_frame = self._logical_channels[channel_id].queue.get(
+ timeout=self._timeout)
+ except Queue.Empty, e:
+ raise Exception('Cannot receive message from channel id %d' %
+ channel_id)
+
+ if binary:
+ opcode = client_for_testing.OPCODE_BINARY
+ else:
+ opcode = client_for_testing.OPCODE_TEXT
+
+ if inner_frame.opcode != opcode:
+ raise Exception('Unexpected opcode received (%r != %r)' %
+ (expected_opcode, inner_frame.opcode))
+
+ if inner_frame.payload != payload:
+ raise Exception('Unexpected payload received')
+
+ def send_close(self, channel_id, code=None, reason=''):
+ self._check_logical_channel_is_opened(channel_id)
+
+ if code is not None:
+ body = struct.pack('!H', code) + reason.encode('utf-8')
+ else:
+ body = ''
+
+ first_byte = (1 << 7) | client_for_testing.OPCODE_CLOSE
+ payload = _encode_channel_id(channel_id) + chr(first_byte) + body
+ self._stream.send_binary(payload)
+
+ def assert_receive_close(self, channel_id):
+ self._check_logical_channel_is_opened(channel_id)
+
+ try:
+ inner_frame = self._logical_channels[channel_id].queue.get(
+ timeout=self._timeout)
+ except Queue.Empty, e:
+ raise Exception('Cannot receive message from channel id %d' %
+ channel_id)
+ if inner_frame.opcode != client_for_testing.OPCODE_CLOSE:
+ raise Exception('Didn\'t receive close frame')
+
+ def send_physical_connection_close(self, code=None, reason=''):
+ self._physical_connection_close_event = threading.Event()
+ self._stream.send_close(code, reason)
+
+ # This method can be used only after calling
+ # send_physical_connection_close().
+ def assert_physical_connection_receive_close(
+ self, code=client_for_testing.STATUS_NORMAL_CLOSURE, reason=''):
+ self._physical_connection_close_event.wait(timeout=self._timeout)
+ if (not self._physical_connection_close_event.isSet() or
+ not self._physical_connection_close_message):
+ raise Exception('Didn\'t receive closing handshake')
+
+ def close_socket(self):
+ self._is_active = False
+ self._socket.close()
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/run_all.py b/testing/web-platform/tests/tools/pywebsocket/src/test/run_all.py
new file mode 100755
index 000000000..80a5d87d8
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/run_all.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Run all tests in the same directory.
+
+This suite is expected to be run under pywebsocket's src directory, i.e. the
+directory containing mod_pywebsocket, test, etc.
+
+To change loggin level, please specify --log-level option.
+ python test/run_test.py --log-level debug
+
+To pass any option to unittest module, please specify options after '--'. For
+example, run this for making the test runner verbose.
+ python test/run_test.py --log-level debug -- -v
+"""
+
+
+import logging
+import optparse
+import os
+import re
+import sys
+import unittest
+
+
+_TEST_MODULE_PATTERN = re.compile(r'^(test_.+)\.py$')
+
+
+def _list_test_modules(directory):
+ module_names = []
+ for filename in os.listdir(directory):
+ match = _TEST_MODULE_PATTERN.search(filename)
+ if match:
+ module_names.append(match.group(1))
+ return module_names
+
+
+def _suite():
+ loader = unittest.TestLoader()
+ return loader.loadTestsFromNames(
+ _list_test_modules(os.path.join(os.path.split(__file__)[0], '.')))
+
+
+if __name__ == '__main__':
+ parser = optparse.OptionParser()
+ parser.add_option('--log-level', '--log_level', type='choice',
+ dest='log_level', default='warning',
+ choices=['debug', 'info', 'warning', 'warn', 'error',
+ 'critical'])
+ options, args = parser.parse_args()
+ logging.basicConfig(
+ level=logging.getLevelName(options.log_level.upper()),
+ format='%(levelname)s %(asctime)s '
+ '%(filename)s:%(lineno)d] '
+ '%(message)s',
+ datefmt='%H:%M:%S')
+ unittest.main(defaultTest='_suite', argv=[sys.argv[0]] + args)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/set_sys_path.py b/testing/web-platform/tests/tools/pywebsocket/src/test/set_sys_path.py
new file mode 100644
index 000000000..e3c6db9ea
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/set_sys_path.py
@@ -0,0 +1,45 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Configuration for testing.
+
+Test files should import this module before mod_pywebsocket.
+"""
+
+
+import os
+import sys
+
+
+# Add the parent directory to sys.path to enable importing mod_pywebsocket.
+sys.path.insert(0, os.path.join(os.path.split(__file__)[0], '..'))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_dispatch.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_dispatch.py
new file mode 100755
index 000000000..9ca3d4f3a
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_dispatch.py
@@ -0,0 +1,288 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for dispatch module."""
+
+
+import os
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import dispatch
+from mod_pywebsocket import handshake
+from test import mock
+
+
+_TEST_HANDLERS_DIR = os.path.join(
+ os.path.split(__file__)[0], 'testdata', 'handlers')
+
+_TEST_HANDLERS_SUB_DIR = os.path.join(_TEST_HANDLERS_DIR, 'sub')
+
+
+class DispatcherTest(unittest.TestCase):
+ """A unittest for dispatch module."""
+
+ def test_normalize_path(self):
+ self.assertEqual(os.path.abspath('/a/b').replace('\\', '/'),
+ dispatch._normalize_path('/a/b'))
+ self.assertEqual(os.path.abspath('/a/b').replace('\\', '/'),
+ dispatch._normalize_path('\\a\\b'))
+ self.assertEqual(os.path.abspath('/a/b').replace('\\', '/'),
+ dispatch._normalize_path('/a/c/../b'))
+ self.assertEqual(os.path.abspath('abc').replace('\\', '/'),
+ dispatch._normalize_path('abc'))
+
+ def test_converter(self):
+ converter = dispatch._create_path_to_resource_converter('/a/b')
+ # Python built by MSC inserts a drive name like 'C:\' via realpath().
+ # Converter Generator expands provided path using realpath() and uses
+ # the path including a drive name to verify the prefix.
+ os_root = os.path.realpath('/')
+ self.assertEqual('/h', converter(os_root + 'a/b/h_wsh.py'))
+ self.assertEqual('/c/h', converter(os_root + 'a/b/c/h_wsh.py'))
+ self.assertEqual(None, converter(os_root + 'a/b/h.py'))
+ self.assertEqual(None, converter('a/b/h_wsh.py'))
+
+ converter = dispatch._create_path_to_resource_converter('a/b')
+ self.assertEqual('/h', converter(dispatch._normalize_path(
+ 'a/b/h_wsh.py')))
+
+ converter = dispatch._create_path_to_resource_converter('/a/b///')
+ self.assertEqual('/h', converter(os_root + 'a/b/h_wsh.py'))
+ self.assertEqual('/h', converter(dispatch._normalize_path(
+ '/a/b/../b/h_wsh.py')))
+
+ converter = dispatch._create_path_to_resource_converter(
+ '/a/../a/b/../b/')
+ self.assertEqual('/h', converter(os_root + 'a/b/h_wsh.py'))
+
+ converter = dispatch._create_path_to_resource_converter(r'\a\b')
+ self.assertEqual('/h', converter(os_root + r'a\b\h_wsh.py'))
+ self.assertEqual('/h', converter(os_root + r'a/b/h_wsh.py'))
+
+ def test_enumerate_handler_file_paths(self):
+ paths = list(
+ dispatch._enumerate_handler_file_paths(_TEST_HANDLERS_DIR))
+ paths.sort()
+ self.assertEqual(8, len(paths))
+ expected_paths = [
+ os.path.join(_TEST_HANDLERS_DIR, 'abort_by_user_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'blank_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'origin_check_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'sub',
+ 'exception_in_transfer_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'sub', 'non_callable_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'sub', 'plain_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'sub',
+ 'wrong_handshake_sig_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'sub',
+ 'wrong_transfer_sig_wsh.py'),
+ ]
+ for expected, actual in zip(expected_paths, paths):
+ self.assertEqual(expected, actual)
+
+ def test_source_handler_file(self):
+ self.assertRaises(
+ dispatch.DispatchException, dispatch._source_handler_file, '')
+ self.assertRaises(
+ dispatch.DispatchException, dispatch._source_handler_file, 'def')
+ self.assertRaises(
+ dispatch.DispatchException, dispatch._source_handler_file, '1/0')
+ self.failUnless(dispatch._source_handler_file(
+ 'def web_socket_do_extra_handshake(request):pass\n'
+ 'def web_socket_transfer_data(request):pass\n'))
+
+ def test_source_warnings(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ warnings = dispatcher.source_warnings()
+ warnings.sort()
+ expected_warnings = [
+ (os.path.realpath(os.path.join(
+ _TEST_HANDLERS_DIR, 'blank_wsh.py')) +
+ ': web_socket_do_extra_handshake is not defined.'),
+ (os.path.realpath(os.path.join(
+ _TEST_HANDLERS_DIR, 'sub', 'non_callable_wsh.py')) +
+ ': web_socket_do_extra_handshake is not callable.'),
+ (os.path.realpath(os.path.join(
+ _TEST_HANDLERS_DIR, 'sub', 'wrong_handshake_sig_wsh.py')) +
+ ': web_socket_do_extra_handshake is not defined.'),
+ (os.path.realpath(os.path.join(
+ _TEST_HANDLERS_DIR, 'sub', 'wrong_transfer_sig_wsh.py')) +
+ ': web_socket_transfer_data is not defined.'),
+ ]
+ self.assertEquals(4, len(warnings))
+ for expected, actual in zip(expected_warnings, warnings):
+ self.assertEquals(expected, actual)
+
+ def test_do_extra_handshake(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ request = mock.MockRequest()
+ request.ws_resource = '/origin_check'
+ request.ws_origin = 'http://example.com'
+ dispatcher.do_extra_handshake(request) # Must not raise exception.
+
+ request.ws_origin = 'http://bad.example.com'
+ try:
+ dispatcher.do_extra_handshake(request)
+ self.fail('Could not catch HandshakeException with 403 status')
+ except handshake.HandshakeException, e:
+ self.assertEquals(403, e.status)
+ except Exception, e:
+ self.fail('Unexpected exception: %r' % e)
+
+ def test_abort_extra_handshake(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ request = mock.MockRequest()
+ request.ws_resource = '/abort_by_user'
+ self.assertRaises(handshake.AbortedByUserException,
+ dispatcher.do_extra_handshake, request)
+
+ def test_transfer_data(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+
+ request = mock.MockRequest(connection=mock.MockConn('\xff\x00'))
+ request.ws_resource = '/origin_check'
+ request.ws_protocol = 'p1'
+ dispatcher.transfer_data(request)
+ self.assertEqual('origin_check_wsh.py is called for /origin_check, p1'
+ '\xff\x00',
+ request.connection.written_data())
+
+ request = mock.MockRequest(connection=mock.MockConn('\xff\x00'))
+ request.ws_resource = '/sub/plain'
+ request.ws_protocol = None
+ dispatcher.transfer_data(request)
+ self.assertEqual('sub/plain_wsh.py is called for /sub/plain, None'
+ '\xff\x00',
+ request.connection.written_data())
+
+ request = mock.MockRequest(connection=mock.MockConn('\xff\x00'))
+ request.ws_resource = '/sub/plain?'
+ request.ws_protocol = None
+ dispatcher.transfer_data(request)
+ self.assertEqual('sub/plain_wsh.py is called for /sub/plain?, None'
+ '\xff\x00',
+ request.connection.written_data())
+
+ request = mock.MockRequest(connection=mock.MockConn('\xff\x00'))
+ request.ws_resource = '/sub/plain?q=v'
+ request.ws_protocol = None
+ dispatcher.transfer_data(request)
+ self.assertEqual('sub/plain_wsh.py is called for /sub/plain?q=v, None'
+ '\xff\x00',
+ request.connection.written_data())
+
+ def test_transfer_data_no_handler(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ for resource in ['/blank', '/sub/non_callable',
+ '/sub/no_wsh_at_the_end', '/does/not/exist']:
+ request = mock.MockRequest(connection=mock.MockConn(''))
+ request.ws_resource = resource
+ request.ws_protocol = 'p2'
+ try:
+ dispatcher.transfer_data(request)
+ self.fail()
+ except dispatch.DispatchException, e:
+ self.failUnless(str(e).find('No handler') != -1)
+ except Exception:
+ self.fail()
+
+ def test_transfer_data_handler_exception(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ request = mock.MockRequest(connection=mock.MockConn(''))
+ request.ws_resource = '/sub/exception_in_transfer'
+ request.ws_protocol = 'p3'
+ try:
+ dispatcher.transfer_data(request)
+ self.fail()
+ except Exception, e:
+ self.failUnless(str(e).find('Intentional') != -1,
+ 'Unexpected exception: %s' % e)
+
+ def test_abort_transfer_data(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ request = mock.MockRequest()
+ request.ws_resource = '/abort_by_user'
+ self.assertRaises(handshake.AbortedByUserException,
+ dispatcher.transfer_data, request)
+
+ def test_scan_dir(self):
+ disp = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ self.assertEqual(4, len(disp._handler_suite_map))
+ self.failUnless('/origin_check' in disp._handler_suite_map)
+ self.failUnless(
+ '/sub/exception_in_transfer' in disp._handler_suite_map)
+ self.failUnless('/sub/plain' in disp._handler_suite_map)
+
+ def test_scan_sub_dir(self):
+ disp = dispatch.Dispatcher(_TEST_HANDLERS_DIR, _TEST_HANDLERS_SUB_DIR)
+ self.assertEqual(2, len(disp._handler_suite_map))
+ self.failIf('/origin_check' in disp._handler_suite_map)
+ self.failUnless(
+ '/sub/exception_in_transfer' in disp._handler_suite_map)
+ self.failUnless('/sub/plain' in disp._handler_suite_map)
+
+ def test_scan_sub_dir_as_root(self):
+ disp = dispatch.Dispatcher(_TEST_HANDLERS_SUB_DIR,
+ _TEST_HANDLERS_SUB_DIR)
+ self.assertEqual(2, len(disp._handler_suite_map))
+ self.failIf('/origin_check' in disp._handler_suite_map)
+ self.failIf('/sub/exception_in_transfer' in disp._handler_suite_map)
+ self.failIf('/sub/plain' in disp._handler_suite_map)
+ self.failUnless('/exception_in_transfer' in disp._handler_suite_map)
+ self.failUnless('/plain' in disp._handler_suite_map)
+
+ def test_scan_dir_must_under_root(self):
+ dispatch.Dispatcher('a/b', 'a/b/c') # OK
+ dispatch.Dispatcher('a/b///', 'a/b') # OK
+ self.assertRaises(dispatch.DispatchException,
+ dispatch.Dispatcher, 'a/b/c', 'a/b')
+
+ def test_resource_path_alias(self):
+ disp = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ disp.add_resource_path_alias('/', '/origin_check')
+ self.assertEqual(5, len(disp._handler_suite_map))
+ self.failUnless('/origin_check' in disp._handler_suite_map)
+ self.failUnless(
+ '/sub/exception_in_transfer' in disp._handler_suite_map)
+ self.failUnless('/sub/plain' in disp._handler_suite_map)
+ self.failUnless('/' in disp._handler_suite_map)
+ self.assertRaises(dispatch.DispatchException,
+ disp.add_resource_path_alias, '/alias', '/not-exist')
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_endtoend.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_endtoend.py
new file mode 100755
index 000000000..5e5cf6157
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_endtoend.py
@@ -0,0 +1,753 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""End-to-end tests for pywebsocket. Tests standalone.py by default. You
+can also test mod_pywebsocket hosted on an Apache server by setting
+_use_external_server to True and modifying _external_server_port to point to
+the port on which the Apache server is running.
+"""
+
+
+import logging
+import os
+import signal
+import socket
+import subprocess
+import sys
+import time
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from test import client_for_testing
+from test import mux_client_for_testing
+
+
+# Special message that tells the echo server to start closing handshake
+_GOODBYE_MESSAGE = 'Goodbye'
+
+_SERVER_WARMUP_IN_SEC = 0.2
+
+# If you want to use external server to run end to end tests, set following
+# parameters correctly.
+_use_external_server = False
+_external_server_port = 0
+
+
+# Test body functions
+def _echo_check_procedure(client):
+ client.connect()
+
+ client.send_message('test')
+ client.assert_receive('test')
+ client.send_message('helloworld')
+ client.assert_receive('helloworld')
+
+ client.send_close()
+ client.assert_receive_close()
+
+ client.assert_connection_closed()
+
+
+def _echo_check_procedure_with_binary(client):
+ client.connect()
+
+ client.send_message('binary', binary=True)
+ client.assert_receive('binary', binary=True)
+ client.send_message('\x00\x80\xfe\xff\x00\x80', binary=True)
+ client.assert_receive('\x00\x80\xfe\xff\x00\x80', binary=True)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ client.assert_connection_closed()
+
+
+def _echo_check_procedure_with_goodbye(client):
+ client.connect()
+
+ client.send_message('test')
+ client.assert_receive('test')
+
+ client.send_message(_GOODBYE_MESSAGE)
+ client.assert_receive(_GOODBYE_MESSAGE)
+
+ client.assert_receive_close()
+ client.send_close()
+
+ client.assert_connection_closed()
+
+
+def _echo_check_procedure_with_code_and_reason(client, code, reason):
+ client.connect()
+
+ client.send_close(code, reason)
+ client.assert_receive_close(code, reason)
+
+ client.assert_connection_closed()
+
+
+def _unmasked_frame_check_procedure(client):
+ client.connect()
+
+ client.send_message('test', mask=False)
+ client.assert_receive_close(client_for_testing.STATUS_PROTOCOL_ERROR, '')
+
+ client.assert_connection_closed()
+
+
+def _mux_echo_check_procedure(mux_client):
+ mux_client.connect()
+ mux_client.send_flow_control(1, 1024)
+
+ logical_channel_options = client_for_testing.ClientOptions()
+ logical_channel_options.server_host = 'localhost'
+ logical_channel_options.server_port = 80
+ logical_channel_options.origin = 'http://localhost'
+ logical_channel_options.resource = '/echo'
+ mux_client.add_channel(2, logical_channel_options)
+ mux_client.send_flow_control(2, 1024)
+
+ mux_client.send_message(2, 'test')
+ mux_client.assert_receive(2, 'test')
+
+ mux_client.add_channel(3, logical_channel_options)
+ mux_client.send_flow_control(3, 1024)
+
+ mux_client.send_message(2, 'hello')
+ mux_client.send_message(3, 'world')
+ mux_client.assert_receive(2, 'hello')
+ mux_client.assert_receive(3, 'world')
+
+ # Don't send close message on channel id 1 so that server-initiated
+ # closing handshake won't occur.
+ mux_client.send_close(2)
+ mux_client.send_close(3)
+ mux_client.assert_receive_close(2)
+ mux_client.assert_receive_close(3)
+
+ mux_client.send_physical_connection_close()
+ mux_client.assert_physical_connection_receive_close()
+
+
+class EndToEndTestBase(unittest.TestCase):
+ """Base class for end-to-end tests that launch pywebsocket standalone
+ server as a separate process, connect to it using the client_for_testing
+ module, and check if the server behaves correctly by exchanging opening
+ handshake and frames over a TCP connection.
+ """
+
+ def setUp(self):
+ self.server_stderr = None
+ self.top_dir = os.path.join(os.path.split(__file__)[0], '..')
+ os.putenv('PYTHONPATH', os.path.pathsep.join(sys.path))
+ self.standalone_command = os.path.join(
+ self.top_dir, 'mod_pywebsocket', 'standalone.py')
+ self.document_root = os.path.join(self.top_dir, 'example')
+ s = socket.socket()
+ s.bind(('localhost', 0))
+ (_, self.test_port) = s.getsockname()
+ s.close()
+
+ self._options = client_for_testing.ClientOptions()
+ self._options.server_host = 'localhost'
+ self._options.origin = 'http://localhost'
+ self._options.resource = '/echo'
+
+ # TODO(toyoshim): Eliminate launching a standalone server on using
+ # external server.
+
+ if _use_external_server:
+ self._options.server_port = _external_server_port
+ else:
+ self._options.server_port = self.test_port
+
+ # TODO(tyoshino): Use tearDown to kill the server.
+
+ def _run_python_command(self, commandline, stdout=None, stderr=None):
+ return subprocess.Popen([sys.executable] + commandline, close_fds=True,
+ stdout=stdout, stderr=stderr)
+
+ def _run_server(self):
+ args = [self.standalone_command,
+ '-H', 'localhost',
+ '-V', 'localhost',
+ '-p', str(self.test_port),
+ '-P', str(self.test_port),
+ '-d', self.document_root]
+
+ # Inherit the level set to the root logger by test runner.
+ root_logger = logging.getLogger()
+ log_level = root_logger.getEffectiveLevel()
+ if log_level != logging.NOTSET:
+ args.append('--log-level')
+ args.append(logging.getLevelName(log_level).lower())
+
+ return self._run_python_command(args,
+ stderr=self.server_stderr)
+
+ def _kill_process(self, pid):
+ if sys.platform in ('win32', 'cygwin'):
+ subprocess.call(
+ ('taskkill.exe', '/f', '/pid', str(pid)), close_fds=True)
+ else:
+ os.kill(pid, signal.SIGKILL)
+
+
+class EndToEndHyBiTest(EndToEndTestBase):
+ def setUp(self):
+ EndToEndTestBase.setUp(self)
+
+ def _run_test_with_client_options(self, test_function, options):
+ server = self._run_server()
+ try:
+ # TODO(tyoshino): add some logic to poll the server until it
+ # becomes ready
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ client = client_for_testing.create_client(options)
+ try:
+ test_function(client)
+ finally:
+ client.close_socket()
+ finally:
+ self._kill_process(server.pid)
+
+ def _run_test(self, test_function):
+ self._run_test_with_client_options(test_function, self._options)
+
+ def _run_deflate_frame_test(self, test_function):
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ self._options.enable_deflate_frame()
+ client = client_for_testing.create_client(self._options)
+ try:
+ test_function(client)
+ finally:
+ client.close_socket()
+ finally:
+ self._kill_process(server.pid)
+
+ def _run_permessage_deflate_test(
+ self, offer, response_checker, test_function):
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ self._options.extensions += offer
+ self._options.check_permessage_deflate = response_checker
+ client = client_for_testing.create_client(self._options)
+
+ try:
+ client.connect()
+
+ if test_function is not None:
+ test_function(client)
+
+ client.assert_connection_closed()
+ finally:
+ client.close_socket()
+ finally:
+ self._kill_process(server.pid)
+
+ def _run_close_with_code_and_reason_test(self, test_function, code,
+ reason):
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ client = client_for_testing.create_client(self._options)
+ try:
+ test_function(client, code, reason)
+ finally:
+ client.close_socket()
+ finally:
+ self._kill_process(server.pid)
+
+ def _run_http_fallback_test(self, options, status):
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ client = client_for_testing.create_client(options)
+ try:
+ client.connect()
+ self.fail('Could not catch HttpStatusException')
+ except client_for_testing.HttpStatusException, e:
+ self.assertEqual(status, e.status)
+ except Exception, e:
+ self.fail('Catch unexpected exception')
+ finally:
+ client.close_socket()
+ finally:
+ self._kill_process(server.pid)
+
+ def _run_mux_test(self, test_function):
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ client = mux_client_for_testing.MuxClient(self._options)
+ try:
+ test_function(client)
+ finally:
+ client.close_socket()
+ finally:
+ self._kill_process(server.pid)
+
+ def test_echo(self):
+ self._run_test(_echo_check_procedure)
+
+ def test_echo_binary(self):
+ self._run_test(_echo_check_procedure_with_binary)
+
+ def test_echo_server_close(self):
+ self._run_test(_echo_check_procedure_with_goodbye)
+
+ def test_unmasked_frame(self):
+ self._run_test(_unmasked_frame_check_procedure)
+
+ def test_echo_deflate_frame(self):
+ self._run_deflate_frame_test(_echo_check_procedure)
+
+ def test_echo_deflate_frame_server_close(self):
+ self._run_deflate_frame_test(
+ _echo_check_procedure_with_goodbye)
+
+ def test_echo_permessage_deflate(self):
+ def test_function(client):
+ # From the examples in the spec.
+ compressed_hello = '\xf2\x48\xcd\xc9\xc9\x07\x00'
+ client._stream.send_data(
+ compressed_hello,
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ compressed_hello,
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ self.assertEquals('permessage-deflate', parameter.name())
+ self.assertEquals([], parameter.get_parameters())
+
+ self._run_permessage_deflate_test(
+ ['permessage-deflate'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_two_frames(self):
+ def test_function(client):
+ # From the examples in the spec.
+ client._stream.send_data(
+ '\xf2\x48\xcd',
+ client_for_testing.OPCODE_TEXT,
+ end=False,
+ rsv1=1)
+ client._stream.send_data(
+ '\xc9\xc9\x07\x00',
+ client_for_testing.OPCODE_TEXT)
+ client._stream.assert_receive_binary(
+ '\xf2\x48\xcd\xc9\xc9\x07\x00',
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ self.assertEquals('permessage-deflate', parameter.name())
+ self.assertEquals([], parameter.get_parameters())
+
+ self._run_permessage_deflate_test(
+ ['permessage-deflate'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_two_messages(self):
+ def test_function(client):
+ # From the examples in the spec.
+ client._stream.send_data(
+ '\xf2\x48\xcd\xc9\xc9\x07\x00',
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.send_data(
+ '\xf2\x00\x11\x00\x00',
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ '\xf2\x48\xcd\xc9\xc9\x07\x00',
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ '\xf2\x00\x11\x00\x00',
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ self.assertEquals('permessage-deflate', parameter.name())
+ self.assertEquals([], parameter.get_parameters())
+
+ self._run_permessage_deflate_test(
+ ['permessage-deflate'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_two_msgs_server_no_context_takeover(self):
+ def test_function(client):
+ # From the examples in the spec.
+ client._stream.send_data(
+ '\xf2\x48\xcd\xc9\xc9\x07\x00',
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.send_data(
+ '\xf2\x00\x11\x00\x00',
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ '\xf2\x48\xcd\xc9\xc9\x07\x00',
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ '\xf2\x48\xcd\xc9\xc9\x07\x00',
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ self.assertEquals('permessage-deflate', parameter.name())
+ self.assertEquals([('server_no_context_takeover', None)],
+ parameter.get_parameters())
+
+ self._run_permessage_deflate_test(
+ ['permessage-deflate; server_no_context_takeover'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_preference(self):
+ def test_function(client):
+ # From the examples in the spec.
+ compressed_hello = '\xf2\x48\xcd\xc9\xc9\x07\x00'
+ client._stream.send_data(
+ compressed_hello,
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ compressed_hello,
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ self.assertEquals('permessage-deflate', parameter.name())
+ self.assertEquals([], parameter.get_parameters())
+
+ self._run_permessage_deflate_test(
+ ['permessage-deflate', 'deflate-frame'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_with_parameters(self):
+ def test_function(client):
+ # From the examples in the spec.
+ compressed_hello = '\xf2\x48\xcd\xc9\xc9\x07\x00'
+ client._stream.send_data(
+ compressed_hello,
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ compressed_hello,
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ self.assertEquals('permessage-deflate', parameter.name())
+ self.assertEquals([('server_max_window_bits', '10'),
+ ('server_no_context_takeover', None)],
+ parameter.get_parameters())
+
+ self._run_permessage_deflate_test(
+ ['permessage-deflate; server_max_window_bits=10; '
+ 'server_no_context_takeover'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_with_bad_server_max_window_bits(self):
+ def test_function(client):
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ raise Exception('Unexpected acceptance of permessage-deflate')
+
+ self._run_permessage_deflate_test(
+ ['permessage-deflate; server_max_window_bits=3000000'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_with_bad_server_max_window_bits(self):
+ def test_function(client):
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ raise Exception('Unexpected acceptance of permessage-deflate')
+
+ self._run_permessage_deflate_test(
+ ['permessage-deflate; server_max_window_bits=3000000'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_with_undefined_parameter(self):
+ def test_function(client):
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ raise Exception('Unexpected acceptance of permessage-deflate')
+
+ self._run_permessage_deflate_test(
+ ['permessage-deflate; foo=bar'],
+ response_checker,
+ test_function)
+
+ def test_echo_close_with_code_and_reason(self):
+ self._options.resource = '/close'
+ self._run_close_with_code_and_reason_test(
+ _echo_check_procedure_with_code_and_reason, 3333, 'sunsunsunsun')
+
+ def test_echo_close_with_empty_body(self):
+ self._options.resource = '/close'
+ self._run_close_with_code_and_reason_test(
+ _echo_check_procedure_with_code_and_reason, None, '')
+
+ def test_mux_echo(self):
+ self._run_mux_test(_mux_echo_check_procedure)
+
+ def test_close_on_protocol_error(self):
+ """Tests that the server sends a close frame with protocol error status
+ code when the client sends data with some protocol error.
+ """
+
+ def test_function(client):
+ client.connect()
+
+ # Intermediate frame without any preceding start of fragmentation
+ # frame.
+ client.send_frame_of_arbitrary_bytes('\x80\x80', '')
+ client.assert_receive_close(
+ client_for_testing.STATUS_PROTOCOL_ERROR)
+
+ self._run_test(test_function)
+
+ def test_close_on_unsupported_frame(self):
+ """Tests that the server sends a close frame with unsupported operation
+ status code when the client sends data asking some operation that is
+ not supported by the server.
+ """
+
+ def test_function(client):
+ client.connect()
+
+ # Text frame with RSV3 bit raised.
+ client.send_frame_of_arbitrary_bytes('\x91\x80', '')
+ client.assert_receive_close(
+ client_for_testing.STATUS_UNSUPPORTED_DATA)
+
+ self._run_test(test_function)
+
+ def test_close_on_invalid_frame(self):
+ """Tests that the server sends a close frame with invalid frame payload
+ data status code when the client sends an invalid frame like containing
+ invalid UTF-8 character.
+ """
+
+ def test_function(client):
+ client.connect()
+
+ # Text frame with invalid UTF-8 string.
+ client.send_message('\x80', raw=True)
+ client.assert_receive_close(
+ client_for_testing.STATUS_INVALID_FRAME_PAYLOAD_DATA)
+
+ self._run_test(test_function)
+
+ def test_close_on_internal_endpoint_error(self):
+ """Tests that the server sends a close frame with internal endpoint
+ error status code when the handler does bad operation.
+ """
+
+ self._options.resource = '/internal_error'
+
+ def test_function(client):
+ client.connect()
+ client.assert_receive_close(
+ client_for_testing.STATUS_INTERNAL_ENDPOINT_ERROR)
+
+ self._run_test(test_function)
+
+ # TODO(toyoshim): Add tests to verify invalid absolute uri handling like
+ # host unmatch, port unmatch and invalid port description (':' without port
+ # number).
+
+ def test_absolute_uri(self):
+ """Tests absolute uri request."""
+
+ options = self._options
+ options.resource = 'ws://localhost:%d/echo' % options.server_port
+ self._run_test_with_client_options(_echo_check_procedure, options)
+
+ def test_origin_check(self):
+ """Tests http fallback on origin check fail."""
+
+ options = self._options
+ options.resource = '/origin_check'
+ # Server shows warning message for http 403 fallback. This warning
+ # message is confusing. Following pipe disposes warning messages.
+ self.server_stderr = subprocess.PIPE
+ self._run_http_fallback_test(options, 403)
+
+ def test_version_check(self):
+ """Tests http fallback on version check fail."""
+
+ options = self._options
+ options.version = 99
+ self._run_http_fallback_test(options, 400)
+
+
+class EndToEndHyBi00Test(EndToEndTestBase):
+ def setUp(self):
+ EndToEndTestBase.setUp(self)
+
+ def _run_test(self, test_function):
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ client = client_for_testing.create_client_hybi00(self._options)
+ try:
+ test_function(client)
+ finally:
+ client.close_socket()
+ finally:
+ self._kill_process(server.pid)
+
+ def test_echo(self):
+ self._run_test(_echo_check_procedure)
+
+ def test_echo_server_close(self):
+ self._run_test(_echo_check_procedure_with_goodbye)
+
+
+class EndToEndTestWithEchoClient(EndToEndTestBase):
+ def setUp(self):
+ EndToEndTestBase.setUp(self)
+
+ def _check_example_echo_client_result(
+ self, expected, stdoutdata, stderrdata):
+ actual = stdoutdata.decode("utf-8")
+ if actual != expected:
+ raise Exception('Unexpected result on example echo client: '
+ '%r (expected) vs %r (actual)' %
+ (expected, actual))
+ if stderrdata is not None:
+ raise Exception('Unexpected error message on example echo '
+ 'client: %r' % stderrdata)
+
+ def test_example_echo_client(self):
+ """Tests that the echo_client.py example can talk with the server."""
+
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ client_command = os.path.join(
+ self.top_dir, 'example', 'echo_client.py')
+
+ # Expected output for the default messages.
+ default_expectation = ('Send: Hello\n' 'Recv: Hello\n'
+ u'Send: \u65e5\u672c\n' u'Recv: \u65e5\u672c\n'
+ 'Send close\n' 'Recv ack\n')
+
+ args = [client_command,
+ '-p', str(self._options.server_port)]
+ client = self._run_python_command(args, stdout=subprocess.PIPE)
+ stdoutdata, stderrdata = client.communicate()
+ self._check_example_echo_client_result(
+ default_expectation, stdoutdata, stderrdata)
+
+ # Process a big message for which extended payload length is used.
+ # To handle extended payload length, ws_version attribute will be
+ # accessed. This test checks that ws_version is correctly set.
+ big_message = 'a' * 1024
+ args = [client_command,
+ '-p', str(self._options.server_port),
+ '-m', big_message]
+ client = self._run_python_command(args, stdout=subprocess.PIPE)
+ stdoutdata, stderrdata = client.communicate()
+ expected = ('Send: %s\nRecv: %s\nSend close\nRecv ack\n' %
+ (big_message, big_message))
+ self._check_example_echo_client_result(
+ expected, stdoutdata, stderrdata)
+
+ # Test the permessage-deflate extension.
+ args = [client_command,
+ '-p', str(self._options.server_port),
+ '--use_permessage_deflate']
+ client = self._run_python_command(args, stdout=subprocess.PIPE)
+ stdoutdata, stderrdata = client.communicate()
+ self._check_example_echo_client_result(
+ default_expectation, stdoutdata, stderrdata)
+ finally:
+ self._kill_process(server.pid)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_extensions.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_extensions.py
new file mode 100755
index 000000000..6c8b1262d
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_extensions.py
@@ -0,0 +1,360 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for extensions module."""
+
+
+import unittest
+import zlib
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import common
+from mod_pywebsocket import extensions
+
+
+class ExtensionsTest(unittest.TestCase):
+ """A unittest for non-class methods in extensions.py"""
+
+ def test_parse_window_bits(self):
+ self.assertRaises(ValueError, extensions._parse_window_bits, None)
+ self.assertRaises(ValueError, extensions._parse_window_bits, 'foobar')
+ self.assertRaises(ValueError, extensions._parse_window_bits, ' 8 ')
+ self.assertRaises(ValueError, extensions._parse_window_bits, 'a8a')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '00000')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '00008')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '0x8')
+
+ self.assertRaises(ValueError, extensions._parse_window_bits, '9.5')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '8.0')
+
+ self.assertTrue(extensions._parse_window_bits, '8')
+ self.assertTrue(extensions._parse_window_bits, '15')
+
+ self.assertRaises(ValueError, extensions._parse_window_bits, '-8')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '0')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '7')
+
+ self.assertRaises(ValueError, extensions._parse_window_bits, '16')
+ self.assertRaises(
+ ValueError, extensions._parse_window_bits, '10000000')
+
+
+class CompressionMethodParameterParserTest(unittest.TestCase):
+ """A unittest for _parse_compression_method which parses the compression
+ method description used by perframe-compression and permessage-compression
+ extension in their "method" extension parameter.
+ """
+
+ def test_parse_method_simple(self):
+ method_list = extensions._parse_compression_method('foo')
+ self.assertEqual(1, len(method_list))
+ method = method_list[0]
+ self.assertEqual('foo', method.name())
+ self.assertEqual(0, len(method.get_parameters()))
+
+ def test_parse_method_with_parameter(self):
+ method_list = extensions._parse_compression_method('foo; x; y=10')
+ self.assertEqual(1, len(method_list))
+ method = method_list[0]
+ self.assertEqual('foo', method.name())
+ self.assertEqual(2, len(method.get_parameters()))
+ self.assertTrue(method.has_parameter('x'))
+ self.assertEqual(None, method.get_parameter_value('x'))
+ self.assertTrue(method.has_parameter('y'))
+ self.assertEqual('10', method.get_parameter_value('y'))
+
+ def test_parse_method_with_quoted_parameter(self):
+ method_list = extensions._parse_compression_method(
+ 'foo; x="Hello World"; y=10')
+ self.assertEqual(1, len(method_list))
+ method = method_list[0]
+ self.assertEqual('foo', method.name())
+ self.assertEqual(2, len(method.get_parameters()))
+ self.assertTrue(method.has_parameter('x'))
+ self.assertEqual('Hello World', method.get_parameter_value('x'))
+ self.assertTrue(method.has_parameter('y'))
+ self.assertEqual('10', method.get_parameter_value('y'))
+
+ def test_parse_method_multiple(self):
+ method_list = extensions._parse_compression_method('foo, bar')
+ self.assertEqual(2, len(method_list))
+ self.assertEqual('foo', method_list[0].name())
+ self.assertEqual(0, len(method_list[0].get_parameters()))
+ self.assertEqual('bar', method_list[1].name())
+ self.assertEqual(0, len(method_list[1].get_parameters()))
+
+ def test_parse_method_multiple_methods_with_quoted_parameter(self):
+ method_list = extensions._parse_compression_method(
+ 'foo; x="Hello World", bar; y=10')
+ self.assertEqual(2, len(method_list))
+ self.assertEqual('foo', method_list[0].name())
+ self.assertEqual(1, len(method_list[0].get_parameters()))
+ self.assertTrue(method_list[0].has_parameter('x'))
+ self.assertEqual('Hello World',
+ method_list[0].get_parameter_value('x'))
+ self.assertEqual('bar', method_list[1].name())
+ self.assertEqual(1, len(method_list[1].get_parameters()))
+ self.assertTrue(method_list[1].has_parameter('y'))
+ self.assertEqual('10', method_list[1].get_parameter_value('y'))
+
+ def test_create_method_desc_simple(self):
+ params = common.ExtensionParameter('foo')
+ desc = extensions._create_accepted_method_desc('foo',
+ params.get_parameters())
+ self.assertEqual('foo', desc)
+
+ def test_create_method_desc_with_parameters(self):
+ params = common.ExtensionParameter('foo')
+ params.add_parameter('x', 'Hello, World')
+ params.add_parameter('y', '10')
+ desc = extensions._create_accepted_method_desc('foo',
+ params.get_parameters())
+ self.assertEqual('foo; x="Hello, World"; y=10', desc)
+
+
+class DeflateFrameExtensionProcessorParsingTest(unittest.TestCase):
+ """A unittest for checking that DeflateFrameExtensionProcessor parses given
+ extension parameter correctly.
+ """
+
+ def test_registry(self):
+ processor = extensions.get_extension_processor(
+ common.ExtensionParameter('deflate-frame'))
+ self.assertIsInstance(processor,
+ extensions.DeflateFrameExtensionProcessor)
+
+ processor = extensions.get_extension_processor(
+ common.ExtensionParameter('x-webkit-deflate-frame'))
+ self.assertIsInstance(processor,
+ extensions.DeflateFrameExtensionProcessor)
+
+ def test_minimal_offer(self):
+ processor = extensions.DeflateFrameExtensionProcessor(
+ common.ExtensionParameter('perframe-deflate'))
+
+ response = processor.get_extension_response()
+ self.assertEqual('perframe-deflate', response.name())
+ self.assertEqual(0, len(response.get_parameters()))
+
+ self.assertEqual(zlib.MAX_WBITS,
+ processor._rfc1979_deflater._window_bits)
+ self.assertFalse(processor._rfc1979_deflater._no_context_takeover)
+
+ def test_offer_with_max_window_bits(self):
+ parameter = common.ExtensionParameter('perframe-deflate')
+ parameter.add_parameter('max_window_bits', '10')
+ processor = extensions.DeflateFrameExtensionProcessor(parameter)
+
+ response = processor.get_extension_response()
+ self.assertEqual('perframe-deflate', response.name())
+ self.assertEqual(0, len(response.get_parameters()))
+
+ self.assertEqual(10, processor._rfc1979_deflater._window_bits)
+
+ def test_offer_with_out_of_range_max_window_bits(self):
+ parameter = common.ExtensionParameter('perframe-deflate')
+ parameter.add_parameter('max_window_bits', '0')
+ processor = extensions.DeflateFrameExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+ def test_offer_with_max_window_bits_without_value(self):
+ parameter = common.ExtensionParameter('perframe-deflate')
+ parameter.add_parameter('max_window_bits', None)
+ processor = extensions.DeflateFrameExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+ def test_offer_with_no_context_takeover(self):
+ parameter = common.ExtensionParameter('perframe-deflate')
+ parameter.add_parameter('no_context_takeover', None)
+ processor = extensions.DeflateFrameExtensionProcessor(parameter)
+
+ response = processor.get_extension_response()
+ self.assertEqual('perframe-deflate', response.name())
+ self.assertEqual(0, len(response.get_parameters()))
+
+ self.assertTrue(processor._rfc1979_deflater._no_context_takeover)
+
+ def test_offer_with_no_context_takeover_with_value(self):
+ parameter = common.ExtensionParameter('perframe-deflate')
+ parameter.add_parameter('no_context_takeover', 'foobar')
+ processor = extensions.DeflateFrameExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+ def test_offer_with_unknown_parameter(self):
+ parameter = common.ExtensionParameter('perframe-deflate')
+ parameter.add_parameter('foo', 'bar')
+ processor = extensions.DeflateFrameExtensionProcessor(parameter)
+
+ response = processor.get_extension_response()
+ self.assertEqual('perframe-deflate', response.name())
+ self.assertEqual(0, len(response.get_parameters()))
+
+
+class PerMessageDeflateExtensionProcessorParsingTest(unittest.TestCase):
+ """A unittest for checking that PerMessageDeflateExtensionProcessor parses
+ given extension parameter correctly.
+ """
+
+ def test_registry(self):
+ processor = extensions.get_extension_processor(
+ common.ExtensionParameter('permessage-deflate'))
+ self.assertIsInstance(processor,
+ extensions.PerMessageDeflateExtensionProcessor)
+
+ def test_minimal_offer(self):
+ processor = extensions.PerMessageDeflateExtensionProcessor(
+ common.ExtensionParameter('permessage-deflate'))
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual(0, len(response.get_parameters()))
+
+ self.assertEqual(zlib.MAX_WBITS,
+ processor._rfc1979_deflater._window_bits)
+ self.assertFalse(processor._rfc1979_deflater._no_context_takeover)
+
+ def test_offer_with_max_window_bits(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('server_max_window_bits', '10')
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual([('server_max_window_bits', '10')],
+ response.get_parameters())
+
+ self.assertEqual(10, processor._rfc1979_deflater._window_bits)
+
+ def test_offer_with_out_of_range_max_window_bits(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('server_max_window_bits', '0')
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+ def test_offer_with_max_window_bits_without_value(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('server_max_window_bits', None)
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+ def test_offer_with_no_context_takeover(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('server_no_context_takeover', None)
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual([('server_no_context_takeover', None)],
+ response.get_parameters())
+
+ self.assertTrue(processor._rfc1979_deflater._no_context_takeover)
+
+ def test_offer_with_no_context_takeover_with_value(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('server_no_context_takeover', 'foobar')
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+ def test_offer_with_unknown_parameter(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('foo', 'bar')
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+
+class PerMessageDeflateExtensionProcessorBuildingTest(unittest.TestCase):
+ """A unittest for checking that PerMessageDeflateExtensionProcessor builds
+ a response based on specified options correctly.
+ """
+
+ def test_response_with_max_window_bits(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('client_max_window_bits', None)
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+ processor.set_client_max_window_bits(10)
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual([('client_max_window_bits', '10')],
+ response.get_parameters())
+
+ def test_response_with_max_window_bits_without_client_permission(self):
+ processor = extensions.PerMessageDeflateExtensionProcessor(
+ common.ExtensionParameter('permessage-deflate'))
+ processor.set_client_max_window_bits(10)
+
+ response = processor.get_extension_response()
+ self.assertIsNone(response)
+
+ def test_response_with_true_for_no_context_takeover(self):
+ processor = extensions.PerMessageDeflateExtensionProcessor(
+ common.ExtensionParameter('permessage-deflate'))
+
+ processor.set_client_no_context_takeover(True)
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual([('client_no_context_takeover', None)],
+ response.get_parameters())
+
+ def test_response_with_false_for_no_context_takeover(self):
+ processor = extensions.PerMessageDeflateExtensionProcessor(
+ common.ExtensionParameter('permessage-deflate'))
+
+ processor.set_client_no_context_takeover(False)
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual(0, len(response.get_parameters()))
+
+
+class PerMessageCompressExtensionProcessorTest(unittest.TestCase):
+ def test_registry(self):
+ processor = extensions.get_extension_processor(
+ common.ExtensionParameter('permessage-compress'))
+ self.assertIsInstance(processor,
+ extensions.PerMessageCompressExtensionProcessor)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake.py
new file mode 100755
index 000000000..aa78ac05e
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake.py
@@ -0,0 +1,188 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for handshake._base module."""
+
+
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket.common import ExtensionParameter
+from mod_pywebsocket.common import ExtensionParsingException
+from mod_pywebsocket.common import format_extensions
+from mod_pywebsocket.common import parse_extensions
+from mod_pywebsocket.handshake._base import HandshakeException
+from mod_pywebsocket.handshake._base import validate_subprotocol
+
+
+class ValidateSubprotocolTest(unittest.TestCase):
+ """A unittest for validate_subprotocol method."""
+
+ def test_validate_subprotocol(self):
+ # Should succeed.
+ validate_subprotocol('sample')
+ validate_subprotocol('Sample')
+ validate_subprotocol('sample\x7eprotocol')
+
+ # Should fail.
+ self.assertRaises(HandshakeException,
+ validate_subprotocol,
+ '')
+ self.assertRaises(HandshakeException,
+ validate_subprotocol,
+ 'sample\x09protocol')
+ self.assertRaises(HandshakeException,
+ validate_subprotocol,
+ 'sample\x19protocol')
+ self.assertRaises(HandshakeException,
+ validate_subprotocol,
+ 'sample\x20protocol')
+ self.assertRaises(HandshakeException,
+ validate_subprotocol,
+ 'sample\x7fprotocol')
+ self.assertRaises(HandshakeException,
+ validate_subprotocol,
+ # "Japan" in Japanese
+ u'\u65e5\u672c')
+
+
+_TEST_TOKEN_EXTENSION_DATA = [
+ ('foo', [('foo', [])]),
+ ('foo; bar', [('foo', [('bar', None)])]),
+ ('foo; bar=baz', [('foo', [('bar', 'baz')])]),
+ ('foo; bar=baz; car=cdr', [('foo', [('bar', 'baz'), ('car', 'cdr')])]),
+ ('foo; bar=baz, car; cdr',
+ [('foo', [('bar', 'baz')]), ('car', [('cdr', None)])]),
+ ('a, b, c, d',
+ [('a', []), ('b', []), ('c', []), ('d', [])]),
+ ]
+
+
+_TEST_QUOTED_EXTENSION_DATA = [
+ ('foo; bar=""', [('foo', [('bar', '')])]),
+ ('foo; bar=" baz "', [('foo', [('bar', ' baz ')])]),
+ ('foo; bar=",baz;"', [('foo', [('bar', ',baz;')])]),
+ ('foo; bar="\\\r\\\nbaz"', [('foo', [('bar', '\r\nbaz')])]),
+ ('foo; bar="\\"baz"', [('foo', [('bar', '"baz')])]),
+ ('foo; bar="\xbbbaz"', [('foo', [('bar', '\xbbbaz')])]),
+ ]
+
+
+_TEST_REDUNDANT_TOKEN_EXTENSION_DATA = [
+ ('foo \t ', [('foo', [])]),
+ ('foo; \r\n bar', [('foo', [('bar', None)])]),
+ ('foo; bar=\r\n \r\n baz', [('foo', [('bar', 'baz')])]),
+ ('foo ;bar = baz ', [('foo', [('bar', 'baz')])]),
+ ('foo,bar,,baz', [('foo', []), ('bar', []), ('baz', [])]),
+ ]
+
+
+_TEST_REDUNDANT_QUOTED_EXTENSION_DATA = [
+ ('foo; bar="\r\n \r\n baz"', [('foo', [('bar', ' baz')])]),
+ ]
+
+
+class ExtensionsParserTest(unittest.TestCase):
+
+ def _verify_extension_list(self, expected_list, actual_list):
+ """Verifies that ExtensionParameter objects in actual_list have the
+ same members as extension definitions in expected_list. Extension
+ definition used in this test is a pair of an extension name and a
+ parameter dictionary.
+ """
+
+ self.assertEqual(len(expected_list), len(actual_list))
+ for expected, actual in zip(expected_list, actual_list):
+ (name, parameters) = expected
+ self.assertEqual(name, actual._name)
+ self.assertEqual(parameters, actual._parameters)
+
+ def test_parse(self):
+ for formatted_string, definition in _TEST_TOKEN_EXTENSION_DATA:
+ self._verify_extension_list(
+ definition, parse_extensions(formatted_string))
+
+ def test_parse_quoted_data(self):
+ for formatted_string, definition in _TEST_QUOTED_EXTENSION_DATA:
+ self._verify_extension_list(
+ definition, parse_extensions(formatted_string))
+
+ def test_parse_redundant_data(self):
+ for (formatted_string,
+ definition) in _TEST_REDUNDANT_TOKEN_EXTENSION_DATA:
+ self._verify_extension_list(
+ definition, parse_extensions(formatted_string))
+
+ def test_parse_redundant_quoted_data(self):
+ for (formatted_string,
+ definition) in _TEST_REDUNDANT_QUOTED_EXTENSION_DATA:
+ self._verify_extension_list(
+ definition, parse_extensions(formatted_string))
+
+ def test_parse_bad_data(self):
+ _TEST_BAD_EXTENSION_DATA = [
+ ('foo; ; '),
+ ('foo; a a'),
+ ('foo foo'),
+ (',,,'),
+ ('foo; bar='),
+ ('foo; bar="hoge'),
+ ('foo; bar="a\r"'),
+ ('foo; bar="\\\xff"'),
+ ('foo; bar=\ra'),
+ ]
+
+ for formatted_string in _TEST_BAD_EXTENSION_DATA:
+ self.assertRaises(
+ ExtensionParsingException, parse_extensions, formatted_string)
+
+
+class FormatExtensionsTest(unittest.TestCase):
+
+ def test_format_extensions(self):
+ for formatted_string, definitions in _TEST_TOKEN_EXTENSION_DATA:
+ extensions = []
+ for definition in definitions:
+ (name, parameters) = definition
+ extension = ExtensionParameter(name)
+ extension._parameters = parameters
+ extensions.append(extension)
+ self.assertEqual(
+ formatted_string, format_extensions(extensions))
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi.py
new file mode 100755
index 000000000..6c8713823
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi.py
@@ -0,0 +1,534 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for handshake module."""
+
+
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+from mod_pywebsocket import common
+from mod_pywebsocket.handshake._base import AbortedByUserException
+from mod_pywebsocket.handshake._base import HandshakeException
+from mod_pywebsocket.handshake._base import VersionException
+from mod_pywebsocket.handshake.hybi import Handshaker
+
+import mock
+
+
+class RequestDefinition(object):
+ """A class for holding data for constructing opening handshake strings for
+ testing the opening handshake processor.
+ """
+
+ def __init__(self, method, uri, headers):
+ self.method = method
+ self.uri = uri
+ self.headers = headers
+
+
+def _create_good_request_def():
+ return RequestDefinition(
+ 'GET', '/demo',
+ {'Host': 'server.example.com',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+ 'Sec-WebSocket-Version': '13',
+ 'Origin': 'http://example.com'})
+
+
+def _create_request(request_def):
+ conn = mock.MockConn('')
+ return mock.MockRequest(
+ method=request_def.method,
+ uri=request_def.uri,
+ headers_in=request_def.headers,
+ connection=conn)
+
+
+def _create_handshaker(request):
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ return handshaker
+
+
+class SubprotocolChoosingDispatcher(object):
+ """A dispatcher for testing. This dispatcher sets the i-th subprotocol
+ of requested ones to ws_protocol where i is given on construction as index
+ argument. If index is negative, default_value will be set to ws_protocol.
+ """
+
+ def __init__(self, index, default_value=None):
+ self.index = index
+ self.default_value = default_value
+
+ def do_extra_handshake(self, conn_context):
+ if self.index >= 0:
+ conn_context.ws_protocol = conn_context.ws_requested_protocols[
+ self.index]
+ else:
+ conn_context.ws_protocol = self.default_value
+
+ def transfer_data(self, conn_context):
+ pass
+
+
+class HandshakeAbortedException(Exception):
+ pass
+
+
+class AbortingDispatcher(object):
+ """A dispatcher for testing. This dispatcher raises an exception in
+ do_extra_handshake to reject the request.
+ """
+
+ def do_extra_handshake(self, conn_context):
+ raise HandshakeAbortedException('An exception to reject the request')
+
+ def transfer_data(self, conn_context):
+ pass
+
+
+class AbortedByUserDispatcher(object):
+ """A dispatcher for testing. This dispatcher raises an
+ AbortedByUserException in do_extra_handshake to reject the request.
+ """
+
+ def do_extra_handshake(self, conn_context):
+ raise AbortedByUserException('An AbortedByUserException to reject the '
+ 'request')
+
+ def transfer_data(self, conn_context):
+ pass
+
+
+_EXPECTED_RESPONSE = (
+ 'HTTP/1.1 101 Switching Protocols\r\n'
+ 'Upgrade: websocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n')
+
+
+class HandshakerTest(unittest.TestCase):
+ """A unittest for draft-ietf-hybi-thewebsocketprotocol-06 and later
+ handshake processor.
+ """
+
+ def test_do_handshake(self):
+ request = _create_request(_create_good_request_def())
+ dispatcher = mock.MockDispatcher()
+ handshaker = Handshaker(request, dispatcher)
+ handshaker.do_handshake()
+
+ self.assertTrue(dispatcher.do_extra_handshake_called)
+
+ self.assertEqual(
+ _EXPECTED_RESPONSE, request.connection.written_data())
+ self.assertEqual('/demo', request.ws_resource)
+ self.assertEqual('http://example.com', request.ws_origin)
+ self.assertEqual(None, request.ws_protocol)
+ self.assertEqual(None, request.ws_extensions)
+ self.assertEqual(common.VERSION_HYBI_LATEST, request.ws_version)
+
+ def test_do_handshake_with_extra_headers(self):
+ request_def = _create_good_request_def()
+ # Add headers not related to WebSocket opening handshake.
+ request_def.headers['FooKey'] = 'BarValue'
+ request_def.headers['EmptyKey'] = ''
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(
+ _EXPECTED_RESPONSE, request.connection.written_data())
+
+ def test_do_handshake_with_capitalized_value(self):
+ request_def = _create_good_request_def()
+ request_def.headers['upgrade'] = 'WEBSOCKET'
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(
+ _EXPECTED_RESPONSE, request.connection.written_data())
+
+ request_def = _create_good_request_def()
+ request_def.headers['Connection'] = 'UPGRADE'
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(
+ _EXPECTED_RESPONSE, request.connection.written_data())
+
+ def test_do_handshake_with_multiple_connection_values(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Connection'] = 'Upgrade, keep-alive, , '
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(
+ _EXPECTED_RESPONSE, request.connection.written_data())
+
+ def test_aborting_handshake(self):
+ handshaker = Handshaker(
+ _create_request(_create_good_request_def()),
+ AbortingDispatcher())
+ # do_extra_handshake raises an exception. Check that it's not caught by
+ # do_handshake.
+ self.assertRaises(HandshakeAbortedException, handshaker.do_handshake)
+
+ def test_do_handshake_with_protocol(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Protocol'] = 'chat, superchat'
+
+ request = _create_request(request_def)
+ handshaker = Handshaker(request, SubprotocolChoosingDispatcher(0))
+ handshaker.do_handshake()
+
+ EXPECTED_RESPONSE = (
+ 'HTTP/1.1 101 Switching Protocols\r\n'
+ 'Upgrade: websocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n'
+ 'Sec-WebSocket-Protocol: chat\r\n\r\n')
+
+ self.assertEqual(EXPECTED_RESPONSE, request.connection.written_data())
+ self.assertEqual('chat', request.ws_protocol)
+
+ def test_do_handshake_protocol_not_in_request_but_in_response(self):
+ request_def = _create_good_request_def()
+ request = _create_request(request_def)
+ handshaker = Handshaker(
+ request, SubprotocolChoosingDispatcher(-1, 'foobar'))
+ # No request has been made but ws_protocol is set. HandshakeException
+ # must be raised.
+ self.assertRaises(HandshakeException, handshaker.do_handshake)
+
+ def test_do_handshake_with_protocol_no_protocol_selection(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Protocol'] = 'chat, superchat'
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ # ws_protocol is not set. HandshakeException must be raised.
+ self.assertRaises(HandshakeException, handshaker.do_handshake)
+
+ def test_do_handshake_with_extensions(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Extensions'] = (
+ 'permessage-compress; method=deflate, unknown')
+
+ EXPECTED_RESPONSE = (
+ 'HTTP/1.1 101 Switching Protocols\r\n'
+ 'Upgrade: websocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n'
+ 'Sec-WebSocket-Extensions: permessage-compress; method=deflate\r\n'
+ '\r\n')
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(EXPECTED_RESPONSE, request.connection.written_data())
+ self.assertEqual(1, len(request.ws_extensions))
+ extension = request.ws_extensions[0]
+ self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION,
+ extension.name())
+ self.assertEqual(['method'], extension.get_parameter_names())
+ self.assertEqual('deflate', extension.get_parameter_value('method'))
+ self.assertEqual(1, len(request.ws_extension_processors))
+ self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION,
+ request.ws_extension_processors[0].name())
+
+ def test_do_handshake_with_permessage_compress(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Extensions'] = (
+ 'permessage-compress; method=deflate')
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(1, len(request.ws_extensions))
+ self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION,
+ request.ws_extensions[0].name())
+ self.assertEqual(1, len(request.ws_extension_processors))
+ self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION,
+ request.ws_extension_processors[0].name())
+
+ def test_do_handshake_with_quoted_extensions(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Extensions'] = (
+ 'permessage-compress; method=deflate, , '
+ 'unknown; e = "mc^2"; ma="\r\n \\\rf "; pv=nrt')
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(2, len(request.ws_requested_extensions))
+ first_extension = request.ws_requested_extensions[0]
+ self.assertEqual('permessage-compress', first_extension.name())
+ self.assertEqual(['method'], first_extension.get_parameter_names())
+ self.assertEqual('deflate',
+ first_extension.get_parameter_value('method'))
+ second_extension = request.ws_requested_extensions[1]
+ self.assertEqual('unknown', second_extension.name())
+ self.assertEqual(
+ ['e', 'ma', 'pv'], second_extension.get_parameter_names())
+ self.assertEqual('mc^2', second_extension.get_parameter_value('e'))
+ self.assertEqual(' \rf ', second_extension.get_parameter_value('ma'))
+ self.assertEqual('nrt', second_extension.get_parameter_value('pv'))
+
+ def test_do_handshake_with_optional_headers(self):
+ request_def = _create_good_request_def()
+ request_def.headers['EmptyValue'] = ''
+ request_def.headers['AKey'] = 'AValue'
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(
+ 'AValue', request.headers_in['AKey'])
+ self.assertEqual(
+ '', request.headers_in['EmptyValue'])
+
+ def test_abort_extra_handshake(self):
+ handshaker = Handshaker(
+ _create_request(_create_good_request_def()),
+ AbortedByUserDispatcher())
+ # do_extra_handshake raises an AbortedByUserException. Check that it's
+ # not caught by do_handshake.
+ self.assertRaises(AbortedByUserException, handshaker.do_handshake)
+
+ def test_do_handshake_with_mux_and_deflate_frame(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Extensions'] = ('%s, %s' % (
+ common.MUX_EXTENSION,
+ common.DEFLATE_FRAME_EXTENSION))
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ # mux should be rejected.
+ self.assertEqual(1, len(request.ws_extensions))
+ self.assertEqual(common.DEFLATE_FRAME_EXTENSION,
+ request.ws_extensions[0].name())
+ self.assertEqual(2, len(request.ws_extension_processors))
+ self.assertEqual(common.MUX_EXTENSION,
+ request.ws_extension_processors[0].name())
+ self.assertEqual(common.DEFLATE_FRAME_EXTENSION,
+ request.ws_extension_processors[1].name())
+ self.assertFalse(hasattr(request, 'mux_processor'))
+
+ def test_do_handshake_with_deflate_frame_and_mux(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Extensions'] = ('%s, %s' % (
+ common.DEFLATE_FRAME_EXTENSION,
+ common.MUX_EXTENSION))
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ # mux should be rejected.
+ self.assertEqual(1, len(request.ws_extensions))
+ first_extension = request.ws_extensions[0]
+ self.assertEqual(common.DEFLATE_FRAME_EXTENSION,
+ first_extension.name())
+ self.assertEqual(2, len(request.ws_extension_processors))
+ self.assertEqual(common.DEFLATE_FRAME_EXTENSION,
+ request.ws_extension_processors[0].name())
+ self.assertEqual(common.MUX_EXTENSION,
+ request.ws_extension_processors[1].name())
+ self.assertFalse(hasattr(request, 'mux'))
+
+ def test_do_handshake_with_permessage_compress_and_mux(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Extensions'] = (
+ '%s; method=deflate, %s' % (
+ common.PERMESSAGE_COMPRESSION_EXTENSION,
+ common.MUX_EXTENSION))
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+
+ self.assertEqual(1, len(request.ws_extensions))
+ self.assertEqual(common.MUX_EXTENSION,
+ request.ws_extensions[0].name())
+ self.assertEqual(2, len(request.ws_extension_processors))
+ self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION,
+ request.ws_extension_processors[0].name())
+ self.assertEqual(common.MUX_EXTENSION,
+ request.ws_extension_processors[1].name())
+ self.assertTrue(hasattr(request, 'mux_processor'))
+ self.assertTrue(request.mux_processor.is_active())
+ mux_extensions = request.mux_processor.extensions()
+ self.assertEqual(1, len(mux_extensions))
+ self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION,
+ mux_extensions[0].name())
+
+ def test_do_handshake_with_mux_and_permessage_compress(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Extensions'] = (
+ '%s, %s; method=deflate' % (
+ common.MUX_EXTENSION,
+ common.PERMESSAGE_COMPRESSION_EXTENSION))
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ # mux should be rejected.
+ self.assertEqual(1, len(request.ws_extensions))
+ first_extension = request.ws_extensions[0]
+ self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION,
+ first_extension.name())
+ self.assertEqual(2, len(request.ws_extension_processors))
+ self.assertEqual(common.MUX_EXTENSION,
+ request.ws_extension_processors[0].name())
+ self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION,
+ request.ws_extension_processors[1].name())
+ self.assertFalse(hasattr(request, 'mux_processor'))
+
+ def test_bad_requests(self):
+ bad_cases = [
+ ('HTTP request',
+ RequestDefinition(
+ 'GET', '/demo',
+ {'Host': 'www.google.com',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5;'
+ ' en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3'
+ ' GTB6 GTBA',
+ 'Accept':
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,'
+ '*/*;q=0.8',
+ 'Accept-Language': 'en-us,en;q=0.5',
+ 'Accept-Encoding': 'gzip,deflate',
+ 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
+ 'Keep-Alive': '300',
+ 'Connection': 'keep-alive'}), None, True)]
+
+ request_def = _create_good_request_def()
+ request_def.method = 'POST'
+ bad_cases.append(('Wrong method', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ del request_def.headers['Host']
+ bad_cases.append(('Missing Host', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ del request_def.headers['Upgrade']
+ bad_cases.append(('Missing Upgrade', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Upgrade'] = 'nonwebsocket'
+ bad_cases.append(('Wrong Upgrade', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ del request_def.headers['Connection']
+ bad_cases.append(('Missing Connection', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Connection'] = 'Downgrade'
+ bad_cases.append(('Wrong Connection', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ del request_def.headers['Sec-WebSocket-Key']
+ bad_cases.append(('Missing Sec-WebSocket-Key', request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Key'] = (
+ 'dGhlIHNhbXBsZSBub25jZQ==garbage')
+ bad_cases.append(('Wrong Sec-WebSocket-Key (with garbage on the tail)',
+ request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Key'] = 'YQ==' # BASE64 of 'a'
+ bad_cases.append(
+ ('Wrong Sec-WebSocket-Key (decoded value is not 16 octets long)',
+ request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ # The last character right before == must be any of A, Q, w and g.
+ request_def.headers['Sec-WebSocket-Key'] = (
+ 'AQIDBAUGBwgJCgsMDQ4PEC==')
+ bad_cases.append(
+ ('Wrong Sec-WebSocket-Key (padding bits are not zero)',
+ request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Key'] = (
+ 'dGhlIHNhbXBsZSBub25jZQ==,dGhlIHNhbXBsZSBub25jZQ==')
+ bad_cases.append(
+ ('Wrong Sec-WebSocket-Key (multiple values)',
+ request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ del request_def.headers['Sec-WebSocket-Version']
+ bad_cases.append(('Missing Sec-WebSocket-Version', request_def, None,
+ True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Version'] = '3'
+ bad_cases.append(('Wrong Sec-WebSocket-Version', request_def, None,
+ False))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Version'] = '13, 13'
+ bad_cases.append(('Wrong Sec-WebSocket-Version (multiple values)',
+ request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Protocol'] = 'illegal\x09protocol'
+ bad_cases.append(('Illegal Sec-WebSocket-Protocol',
+ request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Protocol'] = ''
+ bad_cases.append(('Empty Sec-WebSocket-Protocol',
+ request_def, 400, True))
+
+ for (case_name, request_def, expected_status,
+ expect_handshake_exception) in bad_cases:
+ request = _create_request(request_def)
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ try:
+ handshaker.do_handshake()
+ self.fail('No exception thrown for \'%s\' case' % case_name)
+ except HandshakeException, e:
+ self.assertTrue(expect_handshake_exception)
+ self.assertEqual(expected_status, e.status)
+ except VersionException, e:
+ self.assertFalse(expect_handshake_exception)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi00.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi00.py
new file mode 100755
index 000000000..73f9f27ca
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi00.py
@@ -0,0 +1,516 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for handshake.hybi00 module."""
+
+
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket.handshake._base import HandshakeException
+from mod_pywebsocket.handshake.hybi00 import Handshaker
+from mod_pywebsocket.handshake.hybi00 import _validate_subprotocol
+from test import mock
+
+
+_TEST_KEY1 = '4 @1 46546xW%0l 1 5'
+_TEST_KEY2 = '12998 5 Y3 1 .P00'
+_TEST_KEY3 = '^n:ds[4U'
+_TEST_CHALLENGE_RESPONSE = '8jKS\'y:G*Co,Wxa-'
+
+
+_GOOD_REQUEST = (
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3)
+
+_GOOD_REQUEST_CAPITALIZED_HEADER_VALUES = (
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'UPGRADE',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'WEBSOCKET',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3)
+
+_GOOD_REQUEST_CASE_MIXED_HEADER_NAMES = (
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'hOsT': 'example.com',
+ 'cOnNeCtIoN': 'Upgrade',
+ 'sEc-wEbsOcKeT-kEy2': _TEST_KEY2,
+ 'sEc-wEbsOcKeT-pRoToCoL': 'sample',
+ 'uPgRaDe': 'WebSocket',
+ 'sEc-wEbsOcKeT-kEy1': _TEST_KEY1,
+ 'oRiGiN': 'http://example.com',
+ },
+ _TEST_KEY3)
+
+_GOOD_RESPONSE_DEFAULT_PORT = (
+ 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n'
+ 'Upgrade: WebSocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Location: ws://example.com/demo\r\n'
+ 'Sec-WebSocket-Origin: http://example.com\r\n'
+ 'Sec-WebSocket-Protocol: sample\r\n'
+ '\r\n' +
+ _TEST_CHALLENGE_RESPONSE)
+
+_GOOD_RESPONSE_SECURE = (
+ 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n'
+ 'Upgrade: WebSocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Location: wss://example.com/demo\r\n'
+ 'Sec-WebSocket-Origin: http://example.com\r\n'
+ 'Sec-WebSocket-Protocol: sample\r\n'
+ '\r\n' +
+ _TEST_CHALLENGE_RESPONSE)
+
+_GOOD_REQUEST_NONDEFAULT_PORT = (
+ 8081,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com:8081',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3)
+
+_GOOD_RESPONSE_NONDEFAULT_PORT = (
+ 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n'
+ 'Upgrade: WebSocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Location: ws://example.com:8081/demo\r\n'
+ 'Sec-WebSocket-Origin: http://example.com\r\n'
+ 'Sec-WebSocket-Protocol: sample\r\n'
+ '\r\n' +
+ _TEST_CHALLENGE_RESPONSE)
+
+_GOOD_RESPONSE_SECURE_NONDEF = (
+ 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n'
+ 'Upgrade: WebSocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Location: wss://example.com:8081/demo\r\n'
+ 'Sec-WebSocket-Origin: http://example.com\r\n'
+ 'Sec-WebSocket-Protocol: sample\r\n'
+ '\r\n' +
+ _TEST_CHALLENGE_RESPONSE)
+
+_GOOD_REQUEST_NO_PROTOCOL = (
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3)
+
+_GOOD_RESPONSE_NO_PROTOCOL = (
+ 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n'
+ 'Upgrade: WebSocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Location: ws://example.com/demo\r\n'
+ 'Sec-WebSocket-Origin: http://example.com\r\n'
+ '\r\n' +
+ _TEST_CHALLENGE_RESPONSE)
+
+_GOOD_REQUEST_WITH_OPTIONAL_HEADERS = (
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'EmptyValue': '',
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'AKey': 'AValue',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3)
+
+# TODO(tyoshino): Include \r \n in key3, challenge response.
+
+_GOOD_REQUEST_WITH_NONPRINTABLE_KEY = (
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': 'y R2 48 Q1O4 e|BV3 i5 1 u- 65',
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': '36 7 74 i 92 2\'m 9 0G',
+ 'Origin': 'http://example.com',
+ },
+ ''.join(map(chr, [0x01, 0xd1, 0xdd, 0x3b, 0xd1, 0x56, 0x63, 0xff])))
+
+_GOOD_RESPONSE_WITH_NONPRINTABLE_KEY = (
+ 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n'
+ 'Upgrade: WebSocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Location: ws://example.com/demo\r\n'
+ 'Sec-WebSocket-Origin: http://example.com\r\n'
+ 'Sec-WebSocket-Protocol: sample\r\n'
+ '\r\n' +
+ ''.join(map(chr, [0x0b, 0x99, 0xfa, 0x55, 0xbd, 0x01, 0x23, 0x7b,
+ 0x45, 0xa2, 0xf1, 0xd0, 0x87, 0x8a, 0xee, 0xeb])))
+
+_GOOD_REQUEST_WITH_QUERY_PART = (
+ 80,
+ 'GET',
+ '/demo?e=mc2',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3)
+
+_GOOD_RESPONSE_WITH_QUERY_PART = (
+ 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n'
+ 'Upgrade: WebSocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Location: ws://example.com/demo?e=mc2\r\n'
+ 'Sec-WebSocket-Origin: http://example.com\r\n'
+ 'Sec-WebSocket-Protocol: sample\r\n'
+ '\r\n' +
+ _TEST_CHALLENGE_RESPONSE)
+
+_BAD_REQUESTS = (
+ ( # HTTP request
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'www.google.com',
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5;'
+ ' en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3'
+ ' GTB6 GTBA',
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,'
+ '*/*;q=0.8',
+ 'Accept-Language': 'en-us,en;q=0.5',
+ 'Accept-Encoding': 'gzip,deflate',
+ 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
+ 'Keep-Alive': '300',
+ 'Connection': 'keep-alive',
+ }),
+ ( # Wrong method
+ 80,
+ 'POST',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3),
+ ( # Missing Upgrade
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3),
+ ( # Wrong Upgrade
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'NonWebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3),
+ ( # Empty WebSocket-Protocol
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': '',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3),
+ ( # Wrong port number format
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com:0x50',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3),
+ ( # Header/connection port mismatch
+ 8080,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3),
+ ( # Illegal WebSocket-Protocol
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'illegal\x09protocol',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3),
+)
+
+
+def _create_request(request_def):
+ data = ''
+ if len(request_def) > 4:
+ data = request_def[4]
+ conn = mock.MockConn(data)
+ conn.local_addr = ('0.0.0.0', request_def[0])
+ return mock.MockRequest(
+ method=request_def[1],
+ uri=request_def[2],
+ headers_in=request_def[3],
+ connection=conn)
+
+
+def _create_get_memorized_lines(lines):
+ """Creates a function that returns the given string."""
+
+ def get_memorized_lines():
+ return lines
+ return get_memorized_lines
+
+
+def _create_requests_with_lines(request_lines_set):
+ requests = []
+ for lines in request_lines_set:
+ request = _create_request(_GOOD_REQUEST)
+ request.connection.get_memorized_lines = _create_get_memorized_lines(
+ lines)
+ requests.append(request)
+ return requests
+
+
+class HyBi00HandshakerTest(unittest.TestCase):
+
+ def test_good_request_default_port(self):
+ request = _create_request(_GOOD_REQUEST)
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_DEFAULT_PORT,
+ request.connection.written_data())
+ self.assertEqual('/demo', request.ws_resource)
+ self.assertEqual('http://example.com', request.ws_origin)
+ self.assertEqual('ws://example.com/demo', request.ws_location)
+ self.assertEqual('sample', request.ws_protocol)
+
+ def test_good_request_capitalized_header_values(self):
+ request = _create_request(_GOOD_REQUEST_CAPITALIZED_HEADER_VALUES)
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_DEFAULT_PORT,
+ request.connection.written_data())
+
+ def test_good_request_case_mixed_header_names(self):
+ request = _create_request(_GOOD_REQUEST_CASE_MIXED_HEADER_NAMES)
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_DEFAULT_PORT,
+ request.connection.written_data())
+
+ def test_good_request_secure_default_port(self):
+ request = _create_request(_GOOD_REQUEST)
+ request.connection.local_addr = ('0.0.0.0', 443)
+ request.is_https_ = True
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_SECURE,
+ request.connection.written_data())
+ self.assertEqual('sample', request.ws_protocol)
+
+ def test_good_request_nondefault_port(self):
+ request = _create_request(_GOOD_REQUEST_NONDEFAULT_PORT)
+ handshaker = Handshaker(request,
+ mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_NONDEFAULT_PORT,
+ request.connection.written_data())
+ self.assertEqual('sample', request.ws_protocol)
+
+ def test_good_request_secure_non_default_port(self):
+ request = _create_request(_GOOD_REQUEST_NONDEFAULT_PORT)
+ request.is_https_ = True
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_SECURE_NONDEF,
+ request.connection.written_data())
+ self.assertEqual('sample', request.ws_protocol)
+
+ def test_good_request_default_no_protocol(self):
+ request = _create_request(_GOOD_REQUEST_NO_PROTOCOL)
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_NO_PROTOCOL,
+ request.connection.written_data())
+ self.assertEqual(None, request.ws_protocol)
+
+ def test_good_request_optional_headers(self):
+ request = _create_request(_GOOD_REQUEST_WITH_OPTIONAL_HEADERS)
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual('AValue',
+ request.headers_in['AKey'])
+ self.assertEqual('',
+ request.headers_in['EmptyValue'])
+
+ def test_good_request_with_nonprintable_key(self):
+ request = _create_request(_GOOD_REQUEST_WITH_NONPRINTABLE_KEY)
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_WITH_NONPRINTABLE_KEY,
+ request.connection.written_data())
+ self.assertEqual('sample', request.ws_protocol)
+
+ def test_good_request_with_query_part(self):
+ request = _create_request(_GOOD_REQUEST_WITH_QUERY_PART)
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_WITH_QUERY_PART,
+ request.connection.written_data())
+ self.assertEqual('ws://example.com/demo?e=mc2', request.ws_location)
+
+ def test_bad_requests(self):
+ for request in map(_create_request, _BAD_REQUESTS):
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ self.assertRaises(HandshakeException, handshaker.do_handshake)
+
+
+class HyBi00ValidateSubprotocolTest(unittest.TestCase):
+ def test_validate_subprotocol(self):
+ # should succeed.
+ _validate_subprotocol('sample')
+ _validate_subprotocol('Sample')
+ _validate_subprotocol('sample\x7eprotocol')
+ _validate_subprotocol('sample\x20protocol')
+
+ # should fail.
+ self.assertRaises(HandshakeException,
+ _validate_subprotocol,
+ '')
+ self.assertRaises(HandshakeException,
+ _validate_subprotocol,
+ 'sample\x19protocol')
+ self.assertRaises(HandshakeException,
+ _validate_subprotocol,
+ 'sample\x7fprotocol')
+ self.assertRaises(HandshakeException,
+ _validate_subprotocol,
+ # "Japan" in Japanese
+ u'\u65e5\u672c')
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_http_header_util.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_http_header_util.py
new file mode 100755
index 000000000..436dc57c3
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_http_header_util.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for http_header_util module."""
+
+
+import unittest
+
+from mod_pywebsocket import http_header_util
+
+
+class UnitTest(unittest.TestCase):
+ """A unittest for http_header_util module."""
+
+ def test_parse_relative_uri(self):
+ host, port, resource = http_header_util.parse_uri('/ws/test')
+ self.assertEqual(None, host)
+ self.assertEqual(None, port)
+ self.assertEqual('/ws/test', resource)
+
+ def test_parse_absolute_uri(self):
+ host, port, resource = http_header_util.parse_uri(
+ 'ws://localhost:10080/ws/test')
+ self.assertEqual('localhost', host)
+ self.assertEqual(10080, port)
+ self.assertEqual('/ws/test', resource)
+
+ host, port, resource = http_header_util.parse_uri(
+ 'ws://example.com/ws/test')
+ self.assertEqual('example.com', host)
+ self.assertEqual(80, port)
+ self.assertEqual('/ws/test', resource)
+
+ host, port, resource = http_header_util.parse_uri(
+ 'wss://example.com/')
+ self.assertEqual('example.com', host)
+ self.assertEqual(443, port)
+ self.assertEqual('/', resource)
+
+ host, port, resource = http_header_util.parse_uri(
+ 'ws://example.com:8080')
+ self.assertEqual('example.com', host)
+ self.assertEqual(8080, port)
+ self.assertEqual('/', resource)
+
+ def test_parse_invalid_uri(self):
+ host, port, resource = http_header_util.parse_uri('ws:///')
+ self.assertEqual(None, resource)
+
+ host, port, resource = http_header_util.parse_uri('ws://localhost:')
+ self.assertEqual(None, resource)
+
+ host, port, resource = http_header_util.parse_uri('ws://localhost:/ws')
+ self.assertEqual(None, resource)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_memorizingfile.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_memorizingfile.py
new file mode 100755
index 000000000..8f1b8eef4
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_memorizingfile.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for memorizingfile module."""
+
+
+import StringIO
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import memorizingfile
+
+
+class UtilTest(unittest.TestCase):
+ """A unittest for memorizingfile module."""
+
+ def check(self, memorizing_file, num_read, expected_list):
+ for unused in range(num_read):
+ memorizing_file.readline()
+ actual_list = memorizing_file.get_memorized_lines()
+ self.assertEqual(len(expected_list), len(actual_list))
+ for expected, actual in zip(expected_list, actual_list):
+ self.assertEqual(expected, actual)
+
+ def check_with_size(self, memorizing_file, read_size, expected_list):
+ read_list = []
+ read_line = ''
+ while True:
+ line = memorizing_file.readline(read_size)
+ line_length = len(line)
+ self.assertTrue(line_length <= read_size)
+ if line_length == 0:
+ if read_line != '':
+ read_list.append(read_line)
+ break
+ read_line += line
+ if line[line_length - 1] == '\n':
+ read_list.append(read_line)
+ read_line = ''
+ actual_list = memorizing_file.get_memorized_lines()
+ self.assertEqual(len(expected_list), len(actual_list))
+ self.assertEqual(len(expected_list), len(read_list))
+ for expected, actual, read in zip(expected_list, actual_list,
+ read_list):
+ self.assertEqual(expected, actual)
+ self.assertEqual(expected, read)
+
+ def test_get_memorized_lines(self):
+ memorizing_file = memorizingfile.MemorizingFile(StringIO.StringIO(
+ 'Hello\nWorld\nWelcome'))
+ self.check(memorizing_file, 3, ['Hello\n', 'World\n', 'Welcome'])
+
+ def test_get_memorized_lines_limit_memorized_lines(self):
+ memorizing_file = memorizingfile.MemorizingFile(StringIO.StringIO(
+ 'Hello\nWorld\nWelcome'), 2)
+ self.check(memorizing_file, 3, ['Hello\n', 'World\n'])
+
+ def test_get_memorized_lines_empty_file(self):
+ memorizing_file = memorizingfile.MemorizingFile(StringIO.StringIO(
+ ''))
+ self.check(memorizing_file, 10, [])
+
+ def test_get_memorized_lines_with_size(self):
+ for size in range(1, 10):
+ memorizing_file = memorizingfile.MemorizingFile(StringIO.StringIO(
+ 'Hello\nWorld\nWelcome'))
+ self.check_with_size(memorizing_file, size,
+ ['Hello\n', 'World\n', 'Welcome'])
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_mock.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_mock.py
new file mode 100755
index 000000000..7dc23a73d
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_mock.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for mock module."""
+
+
+import Queue
+import threading
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from test import mock
+
+
+class MockConnTest(unittest.TestCase):
+ """A unittest for MockConn class."""
+
+ def setUp(self):
+ self._conn = mock.MockConn('ABC\r\nDEFG\r\n\r\nHIJK')
+
+ def test_readline(self):
+ self.assertEqual('ABC\r\n', self._conn.readline())
+ self.assertEqual('DEFG\r\n', self._conn.readline())
+ self.assertEqual('\r\n', self._conn.readline())
+ self.assertEqual('HIJK', self._conn.readline())
+ self.assertEqual('', self._conn.readline())
+
+ def test_read(self):
+ self.assertEqual('ABC\r\nD', self._conn.read(6))
+ self.assertEqual('EFG\r\n\r\nHI', self._conn.read(9))
+ self.assertEqual('JK', self._conn.read(10))
+ self.assertEqual('', self._conn.read(10))
+
+ def test_read_and_readline(self):
+ self.assertEqual('ABC\r\nD', self._conn.read(6))
+ self.assertEqual('EFG\r\n', self._conn.readline())
+ self.assertEqual('\r\nHIJK', self._conn.read(9))
+ self.assertEqual('', self._conn.readline())
+
+ def test_write(self):
+ self._conn.write('Hello\r\n')
+ self._conn.write('World\r\n')
+ self.assertEqual('Hello\r\nWorld\r\n', self._conn.written_data())
+
+
+class MockBlockingConnTest(unittest.TestCase):
+ """A unittest for MockBlockingConn class."""
+
+ def test_read(self):
+ """Tests that data put to MockBlockingConn by put_bytes method can be
+ read from it.
+ """
+
+ class LineReader(threading.Thread):
+ """A test class that launches a thread, calls readline on the
+ specified conn repeatedly and puts the read data to the specified
+ queue.
+ """
+
+ def __init__(self, conn, queue):
+ threading.Thread.__init__(self)
+ self._queue = queue
+ self._conn = conn
+ self.setDaemon(True)
+ self.start()
+
+ def run(self):
+ while True:
+ data = self._conn.readline()
+ self._queue.put(data)
+
+ conn = mock.MockBlockingConn()
+ queue = Queue.Queue()
+ reader = LineReader(conn, queue)
+ self.failUnless(queue.empty())
+ conn.put_bytes('Foo bar\r\n')
+ read = queue.get()
+ self.assertEqual('Foo bar\r\n', read)
+
+
+class MockTableTest(unittest.TestCase):
+ """A unittest for MockTable class."""
+
+ def test_create_from_dict(self):
+ table = mock.MockTable({'Key': 'Value'})
+ self.assertEqual('Value', table.get('KEY'))
+ self.assertEqual('Value', table['key'])
+
+ def test_create_from_list(self):
+ table = mock.MockTable([('Key', 'Value')])
+ self.assertEqual('Value', table.get('KEY'))
+ self.assertEqual('Value', table['key'])
+
+ def test_create_from_tuple(self):
+ table = mock.MockTable((('Key', 'Value'),))
+ self.assertEqual('Value', table.get('KEY'))
+ self.assertEqual('Value', table['key'])
+
+ def test_set_and_get(self):
+ table = mock.MockTable()
+ self.assertEqual(None, table.get('Key'))
+ table['Key'] = 'Value'
+ self.assertEqual('Value', table.get('Key'))
+ self.assertEqual('Value', table.get('key'))
+ self.assertEqual('Value', table.get('KEY'))
+ self.assertEqual('Value', table['Key'])
+ self.assertEqual('Value', table['key'])
+ self.assertEqual('Value', table['KEY'])
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_msgutil.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_msgutil.py
new file mode 100755
index 000000000..5fedcf92f
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_msgutil.py
@@ -0,0 +1,1356 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for msgutil module."""
+
+
+import array
+import Queue
+import random
+import struct
+import unittest
+import zlib
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import common
+from mod_pywebsocket.extensions import DeflateFrameExtensionProcessor
+from mod_pywebsocket.extensions import PerMessageCompressExtensionProcessor
+from mod_pywebsocket.extensions import PerMessageDeflateExtensionProcessor
+from mod_pywebsocket import msgutil
+from mod_pywebsocket.stream import InvalidUTF8Exception
+from mod_pywebsocket.stream import Stream
+from mod_pywebsocket.stream import StreamHixie75
+from mod_pywebsocket.stream import StreamOptions
+from mod_pywebsocket import util
+from test import mock
+
+
+# We use one fixed nonce for testing instead of cryptographically secure PRNG.
+_MASKING_NONCE = 'ABCD'
+
+
+def _mask_hybi(frame):
+ frame_key = map(ord, _MASKING_NONCE)
+ frame_key_len = len(frame_key)
+ result = array.array('B')
+ result.fromstring(frame)
+ count = 0
+ for i in xrange(len(result)):
+ result[i] ^= frame_key[count]
+ count = (count + 1) % frame_key_len
+ return _MASKING_NONCE + result.tostring()
+
+
+def _install_extension_processor(processor, request, stream_options):
+ response = processor.get_extension_response()
+ if response is not None:
+ processor.setup_stream_options(stream_options)
+ request.ws_extension_processors.append(processor)
+
+
+def _create_request_from_rawdata(
+ read_data,
+ deflate_frame_request=None,
+ permessage_compression_request=None,
+ permessage_deflate_request=None):
+ req = mock.MockRequest(connection=mock.MockConn(''.join(read_data)))
+ req.ws_version = common.VERSION_HYBI_LATEST
+ req.ws_extension_processors = []
+
+ processor = None
+ if deflate_frame_request is not None:
+ processor = DeflateFrameExtensionProcessor(deflate_frame_request)
+ elif permessage_compression_request is not None:
+ processor = PerMessageCompressExtensionProcessor(
+ permessage_compression_request)
+ elif permessage_deflate_request is not None:
+ processor = PerMessageDeflateExtensionProcessor(
+ permessage_deflate_request)
+
+ stream_options = StreamOptions()
+ if processor is not None:
+ _install_extension_processor(processor, req, stream_options)
+ req.ws_stream = Stream(req, stream_options)
+
+ return req
+
+
+def _create_request(*frames):
+ """Creates MockRequest using data given as frames.
+
+ frames will be returned on calling request.connection.read() where request
+ is MockRequest returned by this function.
+ """
+
+ read_data = []
+ for (header, body) in frames:
+ read_data.append(header + _mask_hybi(body))
+
+ return _create_request_from_rawdata(read_data)
+
+
+def _create_blocking_request():
+ """Creates MockRequest.
+
+ Data written to a MockRequest can be read out by calling
+ request.connection.written_data().
+ """
+
+ req = mock.MockRequest(connection=mock.MockBlockingConn())
+ req.ws_version = common.VERSION_HYBI_LATEST
+ stream_options = StreamOptions()
+ req.ws_stream = Stream(req, stream_options)
+ return req
+
+
+def _create_request_hixie75(read_data=''):
+ req = mock.MockRequest(connection=mock.MockConn(read_data))
+ req.ws_stream = StreamHixie75(req)
+ return req
+
+
+def _create_blocking_request_hixie75():
+ req = mock.MockRequest(connection=mock.MockBlockingConn())
+ req.ws_stream = StreamHixie75(req)
+ return req
+
+
+class BasicMessageTest(unittest.TestCase):
+ """Basic tests for Stream."""
+
+ def test_send_message(self):
+ request = _create_request()
+ msgutil.send_message(request, 'Hello')
+ self.assertEqual('\x81\x05Hello', request.connection.written_data())
+
+ payload = 'a' * 125
+ request = _create_request()
+ msgutil.send_message(request, payload)
+ self.assertEqual('\x81\x7d' + payload,
+ request.connection.written_data())
+
+ def test_send_medium_message(self):
+ payload = 'a' * 126
+ request = _create_request()
+ msgutil.send_message(request, payload)
+ self.assertEqual('\x81\x7e\x00\x7e' + payload,
+ request.connection.written_data())
+
+ payload = 'a' * ((1 << 16) - 1)
+ request = _create_request()
+ msgutil.send_message(request, payload)
+ self.assertEqual('\x81\x7e\xff\xff' + payload,
+ request.connection.written_data())
+
+ def test_send_large_message(self):
+ payload = 'a' * (1 << 16)
+ request = _create_request()
+ msgutil.send_message(request, payload)
+ self.assertEqual('\x81\x7f\x00\x00\x00\x00\x00\x01\x00\x00' + payload,
+ request.connection.written_data())
+
+ def test_send_message_unicode(self):
+ request = _create_request()
+ msgutil.send_message(request, u'\u65e5')
+ # U+65e5 is encoded as e6,97,a5 in UTF-8
+ self.assertEqual('\x81\x03\xe6\x97\xa5',
+ request.connection.written_data())
+
+ def test_send_message_fragments(self):
+ request = _create_request()
+ msgutil.send_message(request, 'Hello', False)
+ msgutil.send_message(request, ' ', False)
+ msgutil.send_message(request, 'World', False)
+ msgutil.send_message(request, '!', True)
+ self.assertEqual('\x01\x05Hello\x00\x01 \x00\x05World\x80\x01!',
+ request.connection.written_data())
+
+ def test_send_fragments_immediate_zero_termination(self):
+ request = _create_request()
+ msgutil.send_message(request, 'Hello World!', False)
+ msgutil.send_message(request, '', True)
+ self.assertEqual('\x01\x0cHello World!\x80\x00',
+ request.connection.written_data())
+
+ def test_receive_message(self):
+ request = _create_request(
+ ('\x81\x85', 'Hello'), ('\x81\x86', 'World!'))
+ self.assertEqual('Hello', msgutil.receive_message(request))
+ self.assertEqual('World!', msgutil.receive_message(request))
+
+ payload = 'a' * 125
+ request = _create_request(('\x81\xfd', payload))
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ def test_receive_medium_message(self):
+ payload = 'a' * 126
+ request = _create_request(('\x81\xfe\x00\x7e', payload))
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ payload = 'a' * ((1 << 16) - 1)
+ request = _create_request(('\x81\xfe\xff\xff', payload))
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ def test_receive_large_message(self):
+ payload = 'a' * (1 << 16)
+ request = _create_request(
+ ('\x81\xff\x00\x00\x00\x00\x00\x01\x00\x00', payload))
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ def test_receive_length_not_encoded_using_minimal_number_of_bytes(self):
+ # Log warning on receiving bad payload length field that doesn't use
+ # minimal number of bytes but continue processing.
+
+ payload = 'a'
+ # 1 byte can be represented without extended payload length field.
+ request = _create_request(
+ ('\x81\xff\x00\x00\x00\x00\x00\x00\x00\x01', payload))
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ def test_receive_message_unicode(self):
+ request = _create_request(('\x81\x83', '\xe6\x9c\xac'))
+ # U+672c is encoded as e6,9c,ac in UTF-8
+ self.assertEqual(u'\u672c', msgutil.receive_message(request))
+
+ def test_receive_message_erroneous_unicode(self):
+ # \x80 and \x81 are invalid as UTF-8.
+ request = _create_request(('\x81\x82', '\x80\x81'))
+ # Invalid characters should raise InvalidUTF8Exception
+ self.assertRaises(InvalidUTF8Exception,
+ msgutil.receive_message,
+ request)
+
+ def test_receive_fragments(self):
+ request = _create_request(
+ ('\x01\x85', 'Hello'),
+ ('\x00\x81', ' '),
+ ('\x00\x85', 'World'),
+ ('\x80\x81', '!'))
+ self.assertEqual('Hello World!', msgutil.receive_message(request))
+
+ def test_receive_fragments_unicode(self):
+ # UTF-8 encodes U+6f22 into e6bca2 and U+5b57 into e5ad97.
+ request = _create_request(
+ ('\x01\x82', '\xe6\xbc'),
+ ('\x00\x82', '\xa2\xe5'),
+ ('\x80\x82', '\xad\x97'))
+ self.assertEqual(u'\u6f22\u5b57', msgutil.receive_message(request))
+
+ def test_receive_fragments_immediate_zero_termination(self):
+ request = _create_request(
+ ('\x01\x8c', 'Hello World!'), ('\x80\x80', ''))
+ self.assertEqual('Hello World!', msgutil.receive_message(request))
+
+ def test_receive_fragments_duplicate_start(self):
+ request = _create_request(
+ ('\x01\x85', 'Hello'), ('\x01\x85', 'World'))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message,
+ request)
+
+ def test_receive_fragments_intermediate_but_not_started(self):
+ request = _create_request(('\x00\x85', 'Hello'))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message,
+ request)
+
+ def test_receive_fragments_end_but_not_started(self):
+ request = _create_request(('\x80\x85', 'Hello'))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message,
+ request)
+
+ def test_receive_message_discard(self):
+ request = _create_request(
+ ('\x8f\x86', 'IGNORE'), ('\x81\x85', 'Hello'),
+ ('\x8f\x89', 'DISREGARD'), ('\x81\x86', 'World!'))
+ self.assertRaises(msgutil.UnsupportedFrameException,
+ msgutil.receive_message, request)
+ self.assertEqual('Hello', msgutil.receive_message(request))
+ self.assertRaises(msgutil.UnsupportedFrameException,
+ msgutil.receive_message, request)
+ self.assertEqual('World!', msgutil.receive_message(request))
+
+ def test_receive_close(self):
+ request = _create_request(
+ ('\x88\x8a', struct.pack('!H', 1000) + 'Good bye'))
+ self.assertEqual(None, msgutil.receive_message(request))
+ self.assertEqual(1000, request.ws_close_code)
+ self.assertEqual('Good bye', request.ws_close_reason)
+
+ def test_send_longest_close(self):
+ reason = 'a' * 123
+ request = _create_request(
+ ('\x88\xfd',
+ struct.pack('!H', common.STATUS_NORMAL_CLOSURE) + reason))
+ request.ws_stream.close_connection(common.STATUS_NORMAL_CLOSURE,
+ reason)
+ self.assertEqual(request.ws_close_code, common.STATUS_NORMAL_CLOSURE)
+ self.assertEqual(request.ws_close_reason, reason)
+
+ def test_send_close_too_long(self):
+ request = _create_request()
+ self.assertRaises(msgutil.BadOperationException,
+ Stream.close_connection,
+ request.ws_stream,
+ common.STATUS_NORMAL_CLOSURE,
+ 'a' * 124)
+
+ def test_send_close_inconsistent_code_and_reason(self):
+ request = _create_request()
+ # reason parameter must not be specified when code is None.
+ self.assertRaises(msgutil.BadOperationException,
+ Stream.close_connection,
+ request.ws_stream,
+ None,
+ 'a')
+
+ def test_send_ping(self):
+ request = _create_request()
+ msgutil.send_ping(request, 'Hello World!')
+ self.assertEqual('\x89\x0cHello World!',
+ request.connection.written_data())
+
+ def test_send_longest_ping(self):
+ request = _create_request()
+ msgutil.send_ping(request, 'a' * 125)
+ self.assertEqual('\x89\x7d' + 'a' * 125,
+ request.connection.written_data())
+
+ def test_send_ping_too_long(self):
+ request = _create_request()
+ self.assertRaises(msgutil.BadOperationException,
+ msgutil.send_ping,
+ request,
+ 'a' * 126)
+
+ def test_receive_ping(self):
+ """Tests receiving a ping control frame."""
+
+ def handler(request, message):
+ request.called = True
+
+ # Stream automatically respond to ping with pong without any action
+ # by application layer.
+ request = _create_request(
+ ('\x89\x85', 'Hello'), ('\x81\x85', 'World'))
+ self.assertEqual('World', msgutil.receive_message(request))
+ self.assertEqual('\x8a\x05Hello',
+ request.connection.written_data())
+
+ request = _create_request(
+ ('\x89\x85', 'Hello'), ('\x81\x85', 'World'))
+ request.on_ping_handler = handler
+ self.assertEqual('World', msgutil.receive_message(request))
+ self.assertTrue(request.called)
+
+ def test_receive_longest_ping(self):
+ request = _create_request(
+ ('\x89\xfd', 'a' * 125), ('\x81\x85', 'World'))
+ self.assertEqual('World', msgutil.receive_message(request))
+ self.assertEqual('\x8a\x7d' + 'a' * 125,
+ request.connection.written_data())
+
+ def test_receive_ping_too_long(self):
+ request = _create_request(('\x89\xfe\x00\x7e', 'a' * 126))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message,
+ request)
+
+ def test_receive_pong(self):
+ """Tests receiving a pong control frame."""
+
+ def handler(request, message):
+ request.called = True
+
+ request = _create_request(
+ ('\x8a\x85', 'Hello'), ('\x81\x85', 'World'))
+ request.on_pong_handler = handler
+ msgutil.send_ping(request, 'Hello')
+ self.assertEqual('\x89\x05Hello',
+ request.connection.written_data())
+ # Valid pong is received, but receive_message won't return for it.
+ self.assertEqual('World', msgutil.receive_message(request))
+ # Check that nothing was written after receive_message call.
+ self.assertEqual('\x89\x05Hello',
+ request.connection.written_data())
+
+ self.assertTrue(request.called)
+
+ def test_receive_unsolicited_pong(self):
+ # Unsolicited pong is allowed from HyBi 07.
+ request = _create_request(
+ ('\x8a\x85', 'Hello'), ('\x81\x85', 'World'))
+ msgutil.receive_message(request)
+
+ request = _create_request(
+ ('\x8a\x85', 'Hello'), ('\x81\x85', 'World'))
+ msgutil.send_ping(request, 'Jumbo')
+ # Body mismatch.
+ msgutil.receive_message(request)
+
+ def test_ping_cannot_be_fragmented(self):
+ request = _create_request(('\x09\x85', 'Hello'))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message,
+ request)
+
+ def test_ping_with_too_long_payload(self):
+ request = _create_request(('\x89\xfe\x01\x00', 'a' * 256))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message,
+ request)
+
+
+class DeflateFrameTest(unittest.TestCase):
+ """Tests for checking deflate-frame extension."""
+
+ def test_send_message(self):
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', deflate_frame_request=extension)
+ msgutil.send_message(request, 'Hello')
+ msgutil.send_message(request, 'World')
+
+ expected = ''
+
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ expected += '\xc1%c' % len(compressed_hello)
+ expected += compressed_hello
+
+ compressed_world = compress.compress('World')
+ compressed_world += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_world = compressed_world[:-4]
+ expected += '\xc1%c' % len(compressed_world)
+ expected += compressed_world
+
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_bfinal(self):
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', deflate_frame_request=extension)
+ self.assertEquals(1, len(request.ws_extension_processors))
+ deflate_frame_processor = request.ws_extension_processors[0]
+ deflate_frame_processor.set_bfinal(True)
+ msgutil.send_message(request, 'Hello')
+ msgutil.send_message(request, 'World')
+
+ expected = ''
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_FINISH)
+ compressed_hello = compressed_hello + chr(0)
+ expected += '\xc1%c' % len(compressed_hello)
+ expected += compressed_hello
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_world = compress.compress('World')
+ compressed_world += compress.flush(zlib.Z_FINISH)
+ compressed_world = compressed_world + chr(0)
+ expected += '\xc1%c' % len(compressed_world)
+ expected += compressed_world
+
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_comp_bit(self):
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', deflate_frame_request=extension)
+ self.assertEquals(1, len(request.ws_extension_processors))
+ deflate_frame_processor = request.ws_extension_processors[0]
+ msgutil.send_message(request, 'Hello')
+ deflate_frame_processor.disable_outgoing_compression()
+ msgutil.send_message(request, 'Hello')
+ deflate_frame_processor.enable_outgoing_compression()
+ msgutil.send_message(request, 'Hello')
+
+ expected = ''
+
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ expected += '\xc1%c' % len(compressed_hello)
+ expected += compressed_hello
+
+ expected += '\x81\x05Hello'
+
+ compressed_2nd_hello = compress.compress('Hello')
+ compressed_2nd_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_2nd_hello = compressed_2nd_hello[:-4]
+ expected += '\xc1%c' % len(compressed_2nd_hello)
+ expected += compressed_2nd_hello
+
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_no_context_takeover_parameter(self):
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ extension.add_parameter('no_context_takeover', None)
+ request = _create_request_from_rawdata(
+ '', deflate_frame_request=extension)
+ for i in xrange(3):
+ msgutil.send_message(request, 'Hello')
+
+ compressed_message = compress.compress('Hello')
+ compressed_message += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_message = compressed_message[:-4]
+ expected = '\xc1%c' % len(compressed_message)
+ expected += compressed_message
+
+ self.assertEqual(
+ expected + expected + expected, request.connection.written_data())
+
+ def test_bad_request_parameters(self):
+ """Tests that if there's anything wrong with deflate-frame extension
+ request, deflate-frame is rejected.
+ """
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ # max_window_bits less than 8 is illegal.
+ extension.add_parameter('max_window_bits', '7')
+ processor = DeflateFrameExtensionProcessor(extension)
+ self.assertEqual(None, processor.get_extension_response())
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ # max_window_bits greater than 15 is illegal.
+ extension.add_parameter('max_window_bits', '16')
+ processor = DeflateFrameExtensionProcessor(extension)
+ self.assertEqual(None, processor.get_extension_response())
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ # Non integer max_window_bits is illegal.
+ extension.add_parameter('max_window_bits', 'foobar')
+ processor = DeflateFrameExtensionProcessor(extension)
+ self.assertEqual(None, processor.get_extension_response())
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ # no_context_takeover must not have any value.
+ extension.add_parameter('no_context_takeover', 'foobar')
+ processor = DeflateFrameExtensionProcessor(extension)
+ self.assertEqual(None, processor.get_extension_response())
+
+ def test_response_parameters(self):
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ processor = DeflateFrameExtensionProcessor(extension)
+ processor.set_response_window_bits(8)
+ response = processor.get_extension_response()
+ self.assertTrue(response.has_parameter('max_window_bits'))
+ self.assertEqual('8', response.get_parameter_value('max_window_bits'))
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ processor = DeflateFrameExtensionProcessor(extension)
+ processor.set_response_no_context_takeover(True)
+ response = processor.get_extension_response()
+ self.assertTrue(response.has_parameter('no_context_takeover'))
+ self.assertTrue(
+ response.get_parameter_value('no_context_takeover') is None)
+
+ def test_receive_message(self):
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ data = ''
+
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ data += '\xc1%c' % (len(compressed_hello) | 0x80)
+ data += _mask_hybi(compressed_hello)
+
+ compressed_websocket = compress.compress('WebSocket')
+ compressed_websocket += compress.flush(zlib.Z_FINISH)
+ compressed_websocket += '\x00'
+ data += '\xc1%c' % (len(compressed_websocket) | 0x80)
+ data += _mask_hybi(compressed_websocket)
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ compressed_world = compress.compress('World')
+ compressed_world += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_world = compressed_world[:-4]
+ data += '\xc1%c' % (len(compressed_world) | 0x80)
+ data += _mask_hybi(compressed_world)
+
+ # Close frame
+ data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye')
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, deflate_frame_request=extension)
+ self.assertEqual('Hello', msgutil.receive_message(request))
+ self.assertEqual('WebSocket', msgutil.receive_message(request))
+ self.assertEqual('World', msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+ def test_receive_message_client_using_smaller_window(self):
+ """Test that frames coming from a client which is using smaller window
+ size that the server are correctly received.
+ """
+
+ # Using the smallest window bits of 8 for generating input frames.
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -8)
+
+ data = ''
+
+ # Use a frame whose content is bigger than the clients' DEFLATE window
+ # size before compression. The content mainly consists of 'a' but
+ # repetition of 'b' is put at the head and tail so that if the window
+ # size is big, the head is back-referenced but if small, not.
+ payload = 'b' * 64 + 'a' * 1024 + 'b' * 64
+ compressed_hello = compress.compress(payload)
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ data += '\xc1%c' % (len(compressed_hello) | 0x80)
+ data += _mask_hybi(compressed_hello)
+
+ # Close frame
+ data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye')
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, deflate_frame_request=extension)
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+ def test_receive_message_comp_bit(self):
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ data = ''
+
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ data += '\xc1%c' % (len(compressed_hello) | 0x80)
+ data += _mask_hybi(compressed_hello)
+
+ data += '\x81\x85' + _mask_hybi('Hello')
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ compressed_2nd_hello = compress.compress('Hello')
+ compressed_2nd_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_2nd_hello = compressed_2nd_hello[:-4]
+ data += '\xc1%c' % (len(compressed_2nd_hello) | 0x80)
+ data += _mask_hybi(compressed_2nd_hello)
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, deflate_frame_request=extension)
+ for i in xrange(3):
+ self.assertEqual('Hello', msgutil.receive_message(request))
+
+ def test_receive_message_various_btype(self):
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ data = ''
+
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ data += '\xc1%c' % (len(compressed_hello) | 0x80)
+ data += _mask_hybi(compressed_hello)
+
+ compressed_websocket = compress.compress('WebSocket')
+ compressed_websocket += compress.flush(zlib.Z_FINISH)
+ compressed_websocket += '\x00'
+ data += '\xc1%c' % (len(compressed_websocket) | 0x80)
+ data += _mask_hybi(compressed_websocket)
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ compressed_world = compress.compress('World')
+ compressed_world += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_world = compressed_world[:-4]
+ data += '\xc1%c' % (len(compressed_world) | 0x80)
+ data += _mask_hybi(compressed_world)
+
+ # Close frame
+ data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye')
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, deflate_frame_request=extension)
+ self.assertEqual('Hello', msgutil.receive_message(request))
+ self.assertEqual('WebSocket', msgutil.receive_message(request))
+ self.assertEqual('World', msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+
+class PerMessageDeflateTest(unittest.TestCase):
+ """Tests for permessage-deflate extension."""
+
+ def test_send_message(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+ msgutil.send_message(request, 'Hello')
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ expected = '\xc1%c' % len(compressed_hello)
+ expected += compressed_hello
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_empty_message(self):
+ """Test that an empty message is compressed correctly."""
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+
+ msgutil.send_message(request, '')
+
+ # Payload in binary: 0b00000010 0b00000000
+ # From LSB,
+ # - 1 bit of BFINAL (0)
+ # - 2 bits of BTYPE (01 that means fixed Huffman)
+ # - 7 bits of the first code (0000000 that is the code for the
+ # end-of-block)
+ # - 1 bit of BFINAL (0)
+ # - 2 bits of BTYPE (no compression)
+ # - 3 bits of padding
+ self.assertEqual('\xc1\x02\x02\x00',
+ request.connection.written_data())
+
+ def test_send_message_with_null_character(self):
+ """Test that a simple payload (one null) is framed correctly."""
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+
+ msgutil.send_message(request, '\x00')
+
+ # Payload in binary: 0b01100010 0b00000000 0b00000000
+ # From LSB,
+ # - 1 bit of BFINAL (0)
+ # - 2 bits of BTYPE (01 that means fixed Huffman)
+ # - 8 bits of the first code (00110000 that is the code for the literal
+ # alphabet 0x00)
+ # - 7 bits of the second code (0000000 that is the code for the
+ # end-of-block)
+ # - 1 bit of BFINAL (0)
+ # - 2 bits of BTYPE (no compression)
+ # - 2 bits of padding
+ self.assertEqual('\xc1\x03\x62\x00\x00',
+ request.connection.written_data())
+
+ def test_send_two_messages(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+ msgutil.send_message(request, 'Hello')
+ msgutil.send_message(request, 'World')
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ expected = ''
+
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ expected += '\xc1%c' % len(compressed_hello)
+ expected += compressed_hello
+
+ compressed_world = compress.compress('World')
+ compressed_world += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_world = compressed_world[:-4]
+ expected += '\xc1%c' % len(compressed_world)
+ expected += compressed_world
+
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_fragmented(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+ msgutil.send_message(request, 'Hello', end=False)
+ msgutil.send_message(request, 'Goodbye', end=False)
+ msgutil.send_message(request, 'World')
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ expected = '\x41%c' % len(compressed_hello)
+ expected += compressed_hello
+ compressed_goodbye = compress.compress('Goodbye')
+ compressed_goodbye += compress.flush(zlib.Z_SYNC_FLUSH)
+ expected += '\x00%c' % len(compressed_goodbye)
+ expected += compressed_goodbye
+ compressed_world = compress.compress('World')
+ compressed_world += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_world = compressed_world[:-4]
+ expected += '\x80%c' % len(compressed_world)
+ expected += compressed_world
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_fragmented_empty_first_frame(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+ msgutil.send_message(request, '', end=False)
+ msgutil.send_message(request, 'Hello')
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ expected = '\x41%c' % len(compressed_hello)
+ expected += compressed_hello
+ compressed_empty = compress.compress('Hello')
+ compressed_empty += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_empty = compressed_empty[:-4]
+ expected += '\x80%c' % len(compressed_empty)
+ expected += compressed_empty
+ print '%r' % expected
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_fragmented_empty_last_frame(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+ msgutil.send_message(request, 'Hello', end=False)
+ msgutil.send_message(request, '')
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ expected = '\x41%c' % len(compressed_hello)
+ expected += compressed_hello
+ compressed_empty = compress.compress('')
+ compressed_empty += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_empty = compressed_empty[:-4]
+ expected += '\x80%c' % len(compressed_empty)
+ expected += compressed_empty
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_using_small_window(self):
+ common_part = 'abcdefghijklmnopqrstuvwxyz'
+ test_message = common_part + '-' * 30000 + common_part
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ extension.add_parameter('server_max_window_bits', '8')
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+ msgutil.send_message(request, test_message)
+
+ expected_websocket_header_size = 2
+ expected_websocket_payload_size = 91
+
+ actual_frame = request.connection.written_data()
+ self.assertEqual(expected_websocket_header_size +
+ expected_websocket_payload_size,
+ len(actual_frame))
+ actual_header = actual_frame[0:expected_websocket_header_size]
+ actual_payload = actual_frame[expected_websocket_header_size:]
+
+ self.assertEqual(
+ '\xc1%c' % expected_websocket_payload_size, actual_header)
+ decompress = zlib.decompressobj(-8)
+ decompressed_message = decompress.decompress(
+ actual_payload + '\x00\x00\xff\xff')
+ decompressed_message += decompress.flush()
+ self.assertEqual(test_message, decompressed_message)
+ self.assertEqual(0, len(decompress.unused_data))
+ self.assertEqual(0, len(decompress.unconsumed_tail))
+
+ def test_send_message_no_context_takeover_parameter(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ extension.add_parameter('server_no_context_takeover', None)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+ for i in xrange(3):
+ msgutil.send_message(request, 'Hello', end=False)
+ msgutil.send_message(request, 'Hello', end=True)
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ first_hello = compress.compress('Hello')
+ first_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ expected = '\x41%c' % len(first_hello)
+ expected += first_hello
+ second_hello = compress.compress('Hello')
+ second_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ second_hello = second_hello[:-4]
+ expected += '\x80%c' % len(second_hello)
+ expected += second_hello
+
+ self.assertEqual(
+ expected + expected + expected,
+ request.connection.written_data())
+
+ def test_send_message_fragmented_bfinal(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+ self.assertEquals(1, len(request.ws_extension_processors))
+ request.ws_extension_processors[0].set_bfinal(True)
+ msgutil.send_message(request, 'Hello', end=False)
+ msgutil.send_message(request, 'World', end=True)
+
+ expected = ''
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_FINISH)
+ compressed_hello = compressed_hello + chr(0)
+ expected += '\x41%c' % len(compressed_hello)
+ expected += compressed_hello
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_world = compress.compress('World')
+ compressed_world += compress.flush(zlib.Z_FINISH)
+ compressed_world = compressed_world + chr(0)
+ expected += '\x80%c' % len(compressed_world)
+ expected += compressed_world
+
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_receive_message_deflate(self):
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ data = '\xc1%c' % (len(compressed_hello) | 0x80)
+ data += _mask_hybi(compressed_hello)
+
+ # Close frame
+ data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye')
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, permessage_deflate_request=extension)
+ self.assertEqual('Hello', msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+ def test_receive_message_random_section(self):
+ """Test that a compressed message fragmented into lots of chunks is
+ correctly received.
+ """
+
+ random.seed(a=0)
+ payload = ''.join(
+ [chr(random.randint(0, 255)) for i in xrange(1000)])
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_payload = compress.compress(payload)
+ compressed_payload += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_payload = compressed_payload[:-4]
+
+ # Fragment the compressed payload into lots of frames.
+ bytes_chunked = 0
+ data = ''
+ frame_count = 0
+
+ chunk_sizes = []
+
+ while bytes_chunked < len(compressed_payload):
+ # Make sure that
+ # - the length of chunks are equal or less than 125 so that we can
+ # use 1 octet length header format for all frames.
+ # - at least 10 chunks are created.
+ chunk_size = random.randint(
+ 1, min(125,
+ len(compressed_payload) / 10,
+ len(compressed_payload) - bytes_chunked))
+ chunk_sizes.append(chunk_size)
+ chunk = compressed_payload[
+ bytes_chunked:bytes_chunked + chunk_size]
+ bytes_chunked += chunk_size
+
+ first_octet = 0x00
+ if len(data) == 0:
+ first_octet = first_octet | 0x42
+ if bytes_chunked == len(compressed_payload):
+ first_octet = first_octet | 0x80
+
+ data += '%c%c' % (first_octet, chunk_size | 0x80)
+ data += _mask_hybi(chunk)
+
+ frame_count += 1
+
+ print "Chunk sizes: %r" % chunk_sizes
+ self.assertTrue(len(chunk_sizes) > 10)
+
+ # Close frame
+ data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye')
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, permessage_deflate_request=extension)
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+ def test_receive_two_messages(self):
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ data = ''
+
+ compressed_hello = compress.compress('HelloWebSocket')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ split_position = len(compressed_hello) / 2
+ data += '\x41%c' % (split_position | 0x80)
+ data += _mask_hybi(compressed_hello[:split_position])
+
+ data += '\x80%c' % ((len(compressed_hello) - split_position) | 0x80)
+ data += _mask_hybi(compressed_hello[split_position:])
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ compressed_world = compress.compress('World')
+ compressed_world += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_world = compressed_world[:-4]
+ data += '\xc1%c' % (len(compressed_world) | 0x80)
+ data += _mask_hybi(compressed_world)
+
+ # Close frame
+ data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye')
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, permessage_deflate_request=extension)
+ self.assertEqual('HelloWebSocket', msgutil.receive_message(request))
+ self.assertEqual('World', msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+ def test_receive_message_mixed_btype(self):
+ """Test that a message compressed using lots of DEFLATE blocks with
+ various flush mode is correctly received.
+ """
+
+ random.seed(a=0)
+ payload = ''.join(
+ [chr(random.randint(0, 255)) for i in xrange(1000)])
+
+ compress = None
+
+ # Fragment the compressed payload into lots of frames.
+ bytes_chunked = 0
+ compressed_payload = ''
+
+ chunk_sizes = []
+ methods = []
+ sync_used = False
+ finish_used = False
+
+ while bytes_chunked < len(payload):
+ # Make sure at least 10 chunks are created.
+ chunk_size = random.randint(
+ 1, min(100, len(payload) - bytes_chunked))
+ chunk_sizes.append(chunk_size)
+ chunk = payload[bytes_chunked:bytes_chunked + chunk_size]
+
+ bytes_chunked += chunk_size
+
+ if compress is None:
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION,
+ zlib.DEFLATED,
+ -zlib.MAX_WBITS)
+
+ if bytes_chunked == len(payload):
+ compressed_payload += compress.compress(chunk)
+ compressed_payload += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_payload = compressed_payload[:-4]
+ else:
+ method = random.randint(0, 1)
+ methods.append(method)
+ if method == 0:
+ compressed_payload += compress.compress(chunk)
+ compressed_payload += compress.flush(zlib.Z_SYNC_FLUSH)
+ sync_used = True
+ else:
+ compressed_payload += compress.compress(chunk)
+ compressed_payload += compress.flush(zlib.Z_FINISH)
+ compress = None
+ finish_used = True
+
+ print "Chunk sizes: %r" % chunk_sizes
+ self.assertTrue(len(chunk_sizes) > 10)
+ print "Methods: %r" % methods
+ self.assertTrue(sync_used)
+ self.assertTrue(finish_used)
+
+ self.assertTrue(125 < len(compressed_payload))
+ self.assertTrue(len(compressed_payload) < 65536)
+ data = '\xc2\xfe' + struct.pack('!H', len(compressed_payload))
+ data += _mask_hybi(compressed_payload)
+
+ # Close frame
+ data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye')
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, permessage_deflate_request=extension)
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+
+class PerMessageCompressTest(unittest.TestCase):
+ """Tests for checking permessage-compression extension."""
+
+ def test_deflate_response_parameters(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_COMPRESSION_EXTENSION)
+ extension.add_parameter('method', 'deflate')
+ processor = PerMessageCompressExtensionProcessor(extension)
+ response = processor.get_extension_response()
+ self.assertEqual('deflate',
+ response.get_parameter_value('method'))
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_COMPRESSION_EXTENSION)
+ extension.add_parameter('method', 'deflate')
+ processor = PerMessageCompressExtensionProcessor(extension)
+
+ def _compression_processor_hook(compression_processor):
+ compression_processor.set_client_max_window_bits(8)
+ compression_processor.set_client_no_context_takeover(True)
+ processor.set_compression_processor_hook(
+ _compression_processor_hook)
+ response = processor.get_extension_response()
+ self.assertEqual(
+ 'deflate; client_max_window_bits=8; client_no_context_takeover',
+ response.get_parameter_value('method'))
+
+
+class MessageTestHixie75(unittest.TestCase):
+ """Tests for draft-hixie-thewebsocketprotocol-76 stream class."""
+
+ def test_send_message(self):
+ request = _create_request_hixie75()
+ msgutil.send_message(request, 'Hello')
+ self.assertEqual('\x00Hello\xff', request.connection.written_data())
+
+ def test_send_message_unicode(self):
+ request = _create_request_hixie75()
+ msgutil.send_message(request, u'\u65e5')
+ # U+65e5 is encoded as e6,97,a5 in UTF-8
+ self.assertEqual('\x00\xe6\x97\xa5\xff',
+ request.connection.written_data())
+
+ def test_receive_message(self):
+ request = _create_request_hixie75('\x00Hello\xff\x00World!\xff')
+ self.assertEqual('Hello', msgutil.receive_message(request))
+ self.assertEqual('World!', msgutil.receive_message(request))
+
+ def test_receive_message_unicode(self):
+ request = _create_request_hixie75('\x00\xe6\x9c\xac\xff')
+ # U+672c is encoded as e6,9c,ac in UTF-8
+ self.assertEqual(u'\u672c', msgutil.receive_message(request))
+
+ def test_receive_message_erroneous_unicode(self):
+ # \x80 and \x81 are invalid as UTF-8.
+ request = _create_request_hixie75('\x00\x80\x81\xff')
+ # Invalid characters should be replaced with
+ # U+fffd REPLACEMENT CHARACTER
+ self.assertEqual(u'\ufffd\ufffd', msgutil.receive_message(request))
+
+ def test_receive_message_discard(self):
+ request = _create_request_hixie75('\x80\x06IGNORE\x00Hello\xff'
+ '\x01DISREGARD\xff\x00World!\xff')
+ self.assertEqual('Hello', msgutil.receive_message(request))
+ self.assertEqual('World!', msgutil.receive_message(request))
+
+
+class MessageReceiverTest(unittest.TestCase):
+ """Tests the Stream class using MessageReceiver."""
+
+ def test_queue(self):
+ request = _create_blocking_request()
+ receiver = msgutil.MessageReceiver(request)
+
+ self.assertEqual(None, receiver.receive_nowait())
+
+ request.connection.put_bytes('\x81\x86' + _mask_hybi('Hello!'))
+ self.assertEqual('Hello!', receiver.receive())
+
+ def test_onmessage(self):
+ onmessage_queue = Queue.Queue()
+
+ def onmessage_handler(message):
+ onmessage_queue.put(message)
+
+ request = _create_blocking_request()
+ receiver = msgutil.MessageReceiver(request, onmessage_handler)
+
+ request.connection.put_bytes('\x81\x86' + _mask_hybi('Hello!'))
+ self.assertEqual('Hello!', onmessage_queue.get())
+
+
+class MessageReceiverHixie75Test(unittest.TestCase):
+ """Tests the StreamHixie75 class using MessageReceiver."""
+
+ def test_queue(self):
+ request = _create_blocking_request_hixie75()
+ receiver = msgutil.MessageReceiver(request)
+
+ self.assertEqual(None, receiver.receive_nowait())
+
+ request.connection.put_bytes('\x00Hello!\xff')
+ self.assertEqual('Hello!', receiver.receive())
+
+ def test_onmessage(self):
+ onmessage_queue = Queue.Queue()
+
+ def onmessage_handler(message):
+ onmessage_queue.put(message)
+
+ request = _create_blocking_request_hixie75()
+ receiver = msgutil.MessageReceiver(request, onmessage_handler)
+
+ request.connection.put_bytes('\x00Hello!\xff')
+ self.assertEqual('Hello!', onmessage_queue.get())
+
+
+class MessageSenderTest(unittest.TestCase):
+ """Tests the Stream class using MessageSender."""
+
+ def test_send(self):
+ request = _create_blocking_request()
+ sender = msgutil.MessageSender(request)
+
+ sender.send('World')
+ self.assertEqual('\x81\x05World', request.connection.written_data())
+
+ def test_send_nowait(self):
+ # Use a queue to check the bytes written by MessageSender.
+ # request.connection.written_data() cannot be used here because
+ # MessageSender runs in a separate thread.
+ send_queue = Queue.Queue()
+
+ def write(bytes):
+ send_queue.put(bytes)
+
+ request = _create_blocking_request()
+ request.connection.write = write
+
+ sender = msgutil.MessageSender(request)
+
+ sender.send_nowait('Hello')
+ sender.send_nowait('World')
+ self.assertEqual('\x81\x05Hello', send_queue.get())
+ self.assertEqual('\x81\x05World', send_queue.get())
+
+
+class MessageSenderHixie75Test(unittest.TestCase):
+ """Tests the StreamHixie75 class using MessageSender."""
+
+ def test_send(self):
+ request = _create_blocking_request_hixie75()
+ sender = msgutil.MessageSender(request)
+
+ sender.send('World')
+ self.assertEqual('\x00World\xff', request.connection.written_data())
+
+ def test_send_nowait(self):
+ # Use a queue to check the bytes written by MessageSender.
+ # request.connection.written_data() cannot be used here because
+ # MessageSender runs in a separate thread.
+ send_queue = Queue.Queue()
+
+ def write(bytes):
+ send_queue.put(bytes)
+
+ request = _create_blocking_request_hixie75()
+ request.connection.write = write
+
+ sender = msgutil.MessageSender(request)
+
+ sender.send_nowait('Hello')
+ sender.send_nowait('World')
+ self.assertEqual('\x00Hello\xff', send_queue.get())
+ self.assertEqual('\x00World\xff', send_queue.get())
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_mux.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_mux.py
new file mode 100644
index 000000000..d4598944e
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_mux.py
@@ -0,0 +1,2089 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for mux module."""
+
+import Queue
+import copy
+import logging
+import optparse
+import struct
+import sys
+import unittest
+import time
+import zlib
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import common
+from mod_pywebsocket import mux
+from mod_pywebsocket._stream_base import ConnectionTerminatedException
+from mod_pywebsocket._stream_base import UnsupportedFrameException
+from mod_pywebsocket._stream_hybi import Frame
+from mod_pywebsocket._stream_hybi import Stream
+from mod_pywebsocket._stream_hybi import StreamOptions
+from mod_pywebsocket._stream_hybi import create_binary_frame
+from mod_pywebsocket._stream_hybi import create_close_frame
+from mod_pywebsocket._stream_hybi import create_closing_handshake_body
+from mod_pywebsocket._stream_hybi import parse_frame
+from mod_pywebsocket.extensions import MuxExtensionProcessor
+
+
+import mock
+
+
+_TEST_HEADERS = {'Host': 'server.example.com',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+ 'Sec-WebSocket-Version': '13',
+ 'Origin': 'http://example.com'}
+
+
+class _OutgoingChannelData(object):
+ def __init__(self):
+ self.messages = []
+ self.control_messages = []
+
+ self.builder = mux._InnerMessageBuilder()
+
+class _MockMuxConnection(mock.MockBlockingConn):
+ """Mock class of mod_python connection for mux."""
+
+ def __init__(self):
+ mock.MockBlockingConn.__init__(self)
+ self._control_blocks = []
+ self._channel_data = {}
+
+ self._current_opcode = None
+ self._pending_fragments = []
+
+ self.server_close_code = None
+
+ def write(self, data):
+ """Override MockBlockingConn.write."""
+
+ self._current_data = data
+ self._position = 0
+
+ def _receive_bytes(length):
+ if self._position + length > len(self._current_data):
+ raise ConnectionTerminatedException(
+ 'Failed to receive %d bytes from encapsulated '
+ 'frame' % length)
+ data = self._current_data[self._position:self._position+length]
+ self._position += length
+ return data
+
+ # Parse physical frames and assemble a message if the message is
+ # fragmented.
+ opcode, payload, fin, rsv1, rsv2, rsv3 = (
+ parse_frame(_receive_bytes, unmask_receive=False))
+
+ self._pending_fragments.append(payload)
+
+ if self._current_opcode is None:
+ if opcode == common.OPCODE_CONTINUATION:
+ raise Exception('Sending invalid continuation opcode')
+ self._current_opcode = opcode
+ else:
+ if opcode != common.OPCODE_CONTINUATION:
+ raise Exception('Sending invalid opcode %d' % opcode)
+ if not fin:
+ return
+
+ inner_frame_data = ''.join(self._pending_fragments)
+ self._pending_fragments = []
+ self._current_opcode = None
+
+ # Handle a control message on the physical channel.
+ # TODO(bashi): Support other opcodes if needed.
+ if opcode == common.OPCODE_CLOSE:
+ if len(payload) >= 2:
+ self.server_close_code = struct.unpack('!H', payload[:2])[0]
+ close_body = create_closing_handshake_body(
+ common.STATUS_NORMAL_CLOSURE, '')
+ close_frame = create_close_frame(close_body, mask=True)
+ self.put_bytes(close_frame)
+ return
+
+ # Parse the payload of the message on physical channel.
+ parser = mux._MuxFramePayloadParser(inner_frame_data)
+ channel_id = parser.read_channel_id()
+ if channel_id == mux._CONTROL_CHANNEL_ID:
+ self._control_blocks.extend(list(parser.read_control_blocks()))
+ return
+
+ if not channel_id in self._channel_data:
+ self._channel_data[channel_id] = _OutgoingChannelData()
+ channel_data = self._channel_data[channel_id]
+
+ # Parse logical frames and assemble an inner (logical) message.
+ (inner_fin, inner_rsv1, inner_rsv2, inner_rsv3, inner_opcode,
+ inner_payload) = parser.read_inner_frame()
+ inner_frame = Frame(inner_fin, inner_rsv1, inner_rsv2, inner_rsv3,
+ inner_opcode, inner_payload)
+ message = channel_data.builder.build(inner_frame)
+ if message is None:
+ return
+
+ if (message.opcode == common.OPCODE_TEXT or
+ message.opcode == common.OPCODE_BINARY):
+ channel_data.messages.append(message.payload)
+
+ self.on_data_message(message.payload)
+ else:
+ channel_data.control_messages.append(
+ {'opcode': message.opcode,
+ 'message': message.payload})
+
+ def on_data_message(self, message):
+ pass
+
+ def get_written_control_blocks(self):
+ return self._control_blocks
+
+ def get_written_messages(self, channel_id):
+ return self._channel_data[channel_id].messages
+
+ def get_written_control_messages(self, channel_id):
+ return self._channel_data[channel_id].control_messages
+
+
+class _FailOnWriteConnection(_MockMuxConnection):
+ """Specicialized version of _MockMuxConnection. Its write() method raises
+ an exception for testing when a data message is written.
+ """
+
+ def on_data_message(self, message):
+ """Override to raise an exception."""
+
+ raise Exception('Intentional failure')
+
+
+class _ChannelEvent(object):
+ """A structure that records channel events."""
+
+ def __init__(self):
+ self.request = None
+ self.messages = []
+ self.exception = None
+ self.client_initiated_closing = False
+
+
+class _MuxMockDispatcher(object):
+ """Mock class of dispatch.Dispatcher for mux."""
+
+ def __init__(self):
+ self.channel_events = {}
+
+ def do_extra_handshake(self, request):
+ if request.ws_requested_protocols is not None:
+ request.ws_protocol = request.ws_requested_protocols[0]
+
+ def _do_echo(self, request, channel_events):
+ while True:
+ message = request.ws_stream.receive_message()
+ if message == None:
+ channel_events.client_initiated_closing = True
+ return
+ if message == 'Goodbye':
+ return
+ channel_events.messages.append(message)
+ # echo back
+ request.ws_stream.send_message(message)
+
+ def _do_ping(self, request, channel_events):
+ request.ws_stream.send_ping('Ping!')
+
+ def _do_ping_while_hello_world(self, request, channel_events):
+ request.ws_stream.send_message('Hello ', end=False)
+ request.ws_stream.send_ping('Ping!')
+ request.ws_stream.send_message('World!', end=True)
+
+ def _do_two_ping_while_hello_world(self, request, channel_events):
+ request.ws_stream.send_message('Hello ', end=False)
+ request.ws_stream.send_ping('Ping!')
+ request.ws_stream.send_ping('Pong!')
+ request.ws_stream.send_message('World!', end=True)
+
+ def transfer_data(self, request):
+ self.channel_events[request.channel_id] = _ChannelEvent()
+ self.channel_events[request.channel_id].request = request
+
+ try:
+ # Note: more handler will be added.
+ if request.uri.endswith('echo'):
+ self._do_echo(request,
+ self.channel_events[request.channel_id])
+ elif request.uri.endswith('ping'):
+ self._do_ping(request,
+ self.channel_events[request.channel_id])
+ elif request.uri.endswith('two_ping_while_hello_world'):
+ self._do_two_ping_while_hello_world(
+ request, self.channel_events[request.channel_id])
+ elif request.uri.endswith('ping_while_hello_world'):
+ self._do_ping_while_hello_world(
+ request, self.channel_events[request.channel_id])
+ else:
+ raise ValueError('Cannot handle path %r' % request.path)
+ if not request.server_terminated:
+ request.ws_stream.close_connection()
+ except ConnectionTerminatedException, e:
+ self.channel_events[request.channel_id].exception = e
+ except Exception, e:
+ self.channel_events[request.channel_id].exception = e
+ raise
+
+
+def _create_mock_request(connection=None, logical_channel_extensions=None):
+ if connection is None:
+ connection = _MockMuxConnection()
+
+ request = mock.MockRequest(uri='/echo',
+ headers_in=_TEST_HEADERS,
+ connection=connection)
+ request.ws_stream = Stream(request, options=StreamOptions())
+ request.mux_processor = MuxExtensionProcessor(
+ common.ExtensionParameter(common.MUX_EXTENSION))
+ if logical_channel_extensions is not None:
+ request.mux_processor.set_extensions(logical_channel_extensions)
+ request.mux_processor.set_quota(8 * 1024)
+ return request
+
+
+def _create_add_channel_request_frame(channel_id, encoding, encoded_handshake):
+ # Allow invalid encoding for testing.
+ first_byte = ((mux._MUX_OPCODE_ADD_CHANNEL_REQUEST << 5) | encoding)
+ payload = (chr(first_byte) +
+ mux._encode_channel_id(channel_id) +
+ mux._encode_number(len(encoded_handshake)) +
+ encoded_handshake)
+ return create_binary_frame(
+ (mux._encode_channel_id(mux._CONTROL_CHANNEL_ID) + payload), mask=True)
+
+
+def _create_drop_channel_frame(channel_id, code=None, message=''):
+ payload = mux._create_drop_channel(channel_id, code, message)
+ return create_binary_frame(
+ (mux._encode_channel_id(mux._CONTROL_CHANNEL_ID) + payload), mask=True)
+
+
+def _create_flow_control_frame(channel_id, replenished_quota):
+ payload = mux._create_flow_control(channel_id, replenished_quota)
+ return create_binary_frame(
+ (mux._encode_channel_id(mux._CONTROL_CHANNEL_ID) + payload), mask=True)
+
+
+def _create_logical_frame(channel_id, message, opcode=common.OPCODE_BINARY,
+ fin=True, rsv1=False, rsv2=False, rsv3=False,
+ mask=True):
+ bits = chr((fin << 7) | (rsv1 << 6) | (rsv2 << 5) | (rsv3 << 4) | opcode)
+ payload = mux._encode_channel_id(channel_id) + bits + message
+ return create_binary_frame(payload, mask=True)
+
+
+def _create_request_header(path='/echo', extensions=None):
+ headers = (
+ 'GET %s HTTP/1.1\r\n'
+ 'Host: server.example.com\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Origin: http://example.com\r\n') % path
+ if extensions:
+ headers += '%s: %s' % (
+ common.SEC_WEBSOCKET_EXTENSIONS_HEADER, extensions)
+ return headers
+
+
+class MuxTest(unittest.TestCase):
+ """A unittest for mux module."""
+
+ def test_channel_id_decode(self):
+ data = '\x00\x01\xbf\xff\xdf\xff\xff\xff\xff\xff\xff'
+ parser = mux._MuxFramePayloadParser(data)
+ channel_id = parser.read_channel_id()
+ self.assertEqual(0, channel_id)
+ channel_id = parser.read_channel_id()
+ self.assertEqual(1, channel_id)
+ channel_id = parser.read_channel_id()
+ self.assertEqual(2 ** 14 - 1, channel_id)
+ channel_id = parser.read_channel_id()
+ self.assertEqual(2 ** 21 - 1, channel_id)
+ channel_id = parser.read_channel_id()
+ self.assertEqual(2 ** 29 - 1, channel_id)
+ self.assertEqual(len(data), parser._read_position)
+
+ def test_channel_id_encode(self):
+ encoded = mux._encode_channel_id(0)
+ self.assertEqual('\x00', encoded)
+ encoded = mux._encode_channel_id(2 ** 14 - 1)
+ self.assertEqual('\xbf\xff', encoded)
+ encoded = mux._encode_channel_id(2 ** 14)
+ self.assertEqual('\xc0@\x00', encoded)
+ encoded = mux._encode_channel_id(2 ** 21 - 1)
+ self.assertEqual('\xdf\xff\xff', encoded)
+ encoded = mux._encode_channel_id(2 ** 21)
+ self.assertEqual('\xe0 \x00\x00', encoded)
+ encoded = mux._encode_channel_id(2 ** 29 - 1)
+ self.assertEqual('\xff\xff\xff\xff', encoded)
+ # channel_id is too large
+ self.assertRaises(ValueError,
+ mux._encode_channel_id,
+ 2 ** 29)
+
+ def test_read_multiple_control_blocks(self):
+ # Use AddChannelRequest because it can contain arbitrary length of data
+ data = ('\x00\x01\x01a'
+ '\x00\x02\x7d%s'
+ '\x00\x03\x7e\xff\xff%s'
+ '\x00\x04\x7f\x00\x00\x00\x00\x00\x01\x00\x00%s') % (
+ 'a' * 0x7d, 'b' * 0xffff, 'c' * 0x10000)
+ parser = mux._MuxFramePayloadParser(data)
+ blocks = list(parser.read_control_blocks())
+ self.assertEqual(4, len(blocks))
+
+ self.assertEqual(mux._MUX_OPCODE_ADD_CHANNEL_REQUEST, blocks[0].opcode)
+ self.assertEqual(1, blocks[0].channel_id)
+ self.assertEqual(1, len(blocks[0].encoded_handshake))
+
+ self.assertEqual(mux._MUX_OPCODE_ADD_CHANNEL_REQUEST, blocks[1].opcode)
+ self.assertEqual(2, blocks[1].channel_id)
+ self.assertEqual(0x7d, len(blocks[1].encoded_handshake))
+
+ self.assertEqual(mux._MUX_OPCODE_ADD_CHANNEL_REQUEST, blocks[2].opcode)
+ self.assertEqual(3, blocks[2].channel_id)
+ self.assertEqual(0xffff, len(blocks[2].encoded_handshake))
+
+ self.assertEqual(mux._MUX_OPCODE_ADD_CHANNEL_REQUEST, blocks[3].opcode)
+ self.assertEqual(4, blocks[3].channel_id)
+ self.assertEqual(0x10000, len(blocks[3].encoded_handshake))
+
+ self.assertEqual(len(data), parser._read_position)
+
+ def test_read_add_channel_request(self):
+ data = '\x00\x01\x01a'
+ parser = mux._MuxFramePayloadParser(data)
+ blocks = list(parser.read_control_blocks())
+ self.assertEqual(mux._MUX_OPCODE_ADD_CHANNEL_REQUEST, blocks[0].opcode)
+ self.assertEqual(1, blocks[0].channel_id)
+ self.assertEqual(1, len(blocks[0].encoded_handshake))
+
+ def test_read_drop_channel(self):
+ data = '\x60\x01\x00'
+ parser = mux._MuxFramePayloadParser(data)
+ blocks = list(parser.read_control_blocks())
+ self.assertEqual(1, len(blocks))
+ self.assertEqual(1, blocks[0].channel_id)
+ self.assertEqual(mux._MUX_OPCODE_DROP_CHANNEL, blocks[0].opcode)
+ self.assertEqual(None, blocks[0].drop_code)
+ self.assertEqual(0, len(blocks[0].drop_message))
+
+ data = '\x60\x02\x09\x03\xe8Success'
+ parser = mux._MuxFramePayloadParser(data)
+ blocks = list(parser.read_control_blocks())
+ self.assertEqual(1, len(blocks))
+ self.assertEqual(2, blocks[0].channel_id)
+ self.assertEqual(mux._MUX_OPCODE_DROP_CHANNEL, blocks[0].opcode)
+ self.assertEqual(1000, blocks[0].drop_code)
+ self.assertEqual('Success', blocks[0].drop_message)
+
+ # Reason is too short.
+ data = '\x60\x01\x01\x00'
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(mux.PhysicalConnectionError,
+ lambda: list(parser.read_control_blocks()))
+
+ def test_read_flow_control(self):
+ data = '\x40\x01\x02'
+ parser = mux._MuxFramePayloadParser(data)
+ blocks = list(parser.read_control_blocks())
+ self.assertEqual(1, len(blocks))
+ self.assertEqual(1, blocks[0].channel_id)
+ self.assertEqual(mux._MUX_OPCODE_FLOW_CONTROL, blocks[0].opcode)
+ self.assertEqual(2, blocks[0].send_quota)
+
+ def test_read_new_channel_slot(self):
+ data = '\x80\x01\x02\x02\x03'
+ parser = mux._MuxFramePayloadParser(data)
+ # TODO(bashi): Implement
+ self.assertRaises(mux.PhysicalConnectionError,
+ lambda: list(parser.read_control_blocks()))
+
+ def test_read_invalid_number_field_in_control_block(self):
+ # No number field.
+ data = ''
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(ValueError, parser._read_number)
+
+ # The last two bytes are missing.
+ data = '\x7e'
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(ValueError, parser._read_number)
+
+ # Missing the last one byte.
+ data = '\x7f\x00\x00\x00\x00\x00\x01\x00'
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(ValueError, parser._read_number)
+
+ # The length of number field is too large.
+ data = '\x7f\xff\xff\xff\xff\xff\xff\xff\xff'
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(ValueError, parser._read_number)
+
+ # The msb of the first byte is set.
+ data = '\x80'
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(ValueError, parser._read_number)
+
+ # Using 3 bytes encoding for 125.
+ data = '\x7e\x00\x7d'
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(ValueError, parser._read_number)
+
+ # Using 9 bytes encoding for 0xffff
+ data = '\x7f\x00\x00\x00\x00\x00\x00\xff\xff'
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(ValueError, parser._read_number)
+
+ def test_read_invalid_size_and_contents(self):
+ # Only contain number field.
+ data = '\x01'
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(mux.PhysicalConnectionError,
+ parser._read_size_and_contents)
+
+ def test_create_add_channel_response(self):
+ data = mux._create_add_channel_response(channel_id=1,
+ encoded_handshake='FooBar',
+ encoding=0,
+ rejected=False)
+ self.assertEqual('\x20\x01\x06FooBar', data)
+
+ data = mux._create_add_channel_response(channel_id=2,
+ encoded_handshake='Hello',
+ encoding=1,
+ rejected=True)
+ self.assertEqual('\x31\x02\x05Hello', data)
+
+ def test_create_drop_channel(self):
+ data = mux._create_drop_channel(channel_id=1)
+ self.assertEqual('\x60\x01\x00', data)
+
+ data = mux._create_drop_channel(channel_id=1,
+ code=2000,
+ message='error')
+ self.assertEqual('\x60\x01\x07\x07\xd0error', data)
+
+ # reason must be empty if code is None
+ self.assertRaises(ValueError,
+ mux._create_drop_channel,
+ 1, None, 'FooBar')
+
+ def test_parse_request_text(self):
+ request_text = _create_request_header()
+ command, path, version, headers = mux._parse_request_text(request_text)
+ self.assertEqual('GET', command)
+ self.assertEqual('/echo', path)
+ self.assertEqual('HTTP/1.1', version)
+ self.assertEqual(3, len(headers))
+ self.assertEqual('server.example.com', headers['Host'])
+ self.assertEqual('http://example.com', headers['Origin'])
+
+
+class MuxHandlerTest(unittest.TestCase):
+
+ def test_add_channel(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=3, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=3,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Hello'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=3, message='World'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=3, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual([], dispatcher.channel_events[1].messages)
+ self.assertEqual(['Hello'], dispatcher.channel_events[2].messages)
+ self.assertEqual(['World'], dispatcher.channel_events[3].messages)
+ # Channel 2
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(1, len(messages))
+ self.assertEqual('Hello', messages[0])
+ # Channel 3
+ messages = request.connection.get_written_messages(3)
+ self.assertEqual(1, len(messages))
+ self.assertEqual('World', messages[0])
+ control_blocks = request.connection.get_written_control_blocks()
+ # There should be 8 control blocks:
+ # - 1 NewChannelSlot
+ # - 2 AddChannelResponses for channel id 2 and 3
+ # - 6 FlowControls for channel id 1 (initialize), 'Hello', 'World',
+ # and 3 'Goodbye's
+ self.assertEqual(9, len(control_blocks))
+
+ def test_physical_connection_write_failure(self):
+ # Use _FailOnWriteConnection.
+ request = _create_mock_request(connection=_FailOnWriteConnection())
+
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+
+ # Let the worker echo back 'Hello'. It causes _FailOnWriteConnection
+ # raising an exception.
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Hello'))
+
+ # Let the worker exit. This will be unnecessary when
+ # _LogicalConnection.write() is changed to throw an exception if
+ # woke up by on_writer_done.
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ # All threads should be done.
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ def test_send_blocked(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # On receiving this 'Hello', the server tries to echo back 'Hello',
+ # but it will be blocked since there's no send quota available for the
+ # channel 2.
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Hello'))
+
+ # Wait until the worker is blocked due to send quota shortage.
+ time.sleep(1)
+
+ # Close the channel 2. The worker should be notified of the end of
+ # writer thread and stop waiting for send quota to be replenished.
+ drop_channel = _create_drop_channel_frame(channel_id=2)
+
+ request.connection.put_bytes(drop_channel)
+
+ # Make sure the channel 1 is also closed.
+ drop_channel = _create_drop_channel_frame(channel_id=1)
+ request.connection.put_bytes(drop_channel)
+
+ # All threads should be done.
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ def test_add_channel_delta_encoding(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ delta = 'GET /echo HTTP/1.1\r\n\r\n'
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=1, encoded_handshake=delta)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Hello'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual(['Hello'], dispatcher.channel_events[2].messages)
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(1, len(messages))
+ self.assertEqual('Hello', messages[0])
+
+ def test_add_channel_delta_encoding_override(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ # Override Sec-WebSocket-Protocol.
+ delta = ('GET /echo HTTP/1.1\r\n'
+ 'Sec-WebSocket-Protocol: x-foo\r\n'
+ '\r\n')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=1, encoded_handshake=delta)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Hello'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual(['Hello'], dispatcher.channel_events[2].messages)
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(1, len(messages))
+ self.assertEqual('Hello', messages[0])
+ self.assertEqual('x-foo',
+ dispatcher.channel_events[2].request.ws_protocol)
+
+ def test_add_channel_delta_after_identity(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+ # Sec-WebSocket-Protocol is different from client's opening handshake
+ # of the physical connection.
+ # TODO(bashi): Remove Upgrade, Connection, Sec-WebSocket-Key and
+ # Sec-WebSocket-Version.
+ encoded_handshake = (
+ 'GET /echo HTTP/1.1\r\n'
+ 'Host: server.example.com\r\n'
+ 'Sec-WebSocket-Protocol: x-foo\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Origin: http://example.com\r\n'
+ '\r\n')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ delta = 'GET /echo HTTP/1.1\r\n\r\n'
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=3, encoding=1, encoded_handshake=delta)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=3,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Hello'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=3, message='World'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=3, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual([], dispatcher.channel_events[1].messages)
+ self.assertEqual(['Hello'], dispatcher.channel_events[2].messages)
+ self.assertEqual(['World'], dispatcher.channel_events[3].messages)
+ # Channel 2
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(1, len(messages))
+ self.assertEqual('Hello', messages[0])
+ # Channel 3
+ messages = request.connection.get_written_messages(3)
+ self.assertEqual(1, len(messages))
+ self.assertEqual('World', messages[0])
+ # Handshake base should be updated.
+ self.assertEqual(
+ 'x-foo',
+ mux_handler._handshake_base._headers['Sec-WebSocket-Protocol'])
+
+ def test_add_channel_delta_remove_header(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+ # Override handshake delta base.
+ encoded_handshake = (
+ 'GET /echo HTTP/1.1\r\n'
+ 'Host: server.example.com\r\n'
+ 'Sec-WebSocket-Protocol: x-foo\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Origin: http://example.com\r\n'
+ '\r\n')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ # Remove Sec-WebSocket-Protocol header.
+ delta = ('GET /echo HTTP/1.1\r\n'
+ 'Sec-WebSocket-Protocol:'
+ '\r\n')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=3, encoding=1, encoded_handshake=delta)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=3,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Hello'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=3, message='World'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=3, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual([], dispatcher.channel_events[1].messages)
+ self.assertEqual(['Hello'], dispatcher.channel_events[2].messages)
+ self.assertEqual(['World'], dispatcher.channel_events[3].messages)
+ # Channel 2
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(1, len(messages))
+ self.assertEqual('Hello', messages[0])
+ # Channel 3
+ messages = request.connection.get_written_messages(3)
+ self.assertEqual(1, len(messages))
+ self.assertEqual('World', messages[0])
+ self.assertEqual(
+ 'x-foo',
+ dispatcher.channel_events[2].request.ws_protocol)
+ self.assertEqual(
+ None,
+ dispatcher.channel_events[3].request.ws_protocol)
+
+ def test_add_channel_delta_encoding_permessage_compress(self):
+ # Enable permessage compress extension on the implicitly opened channel.
+ extensions = common.parse_extensions(
+ '%s; method=deflate' % common.PERMESSAGE_COMPRESSION_EXTENSION)
+ request = _create_mock_request(
+ logical_channel_extensions=extensions)
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ delta = 'GET /echo HTTP/1.1\r\n\r\n'
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=1, encoded_handshake=delta)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=20)
+ request.connection.put_bytes(flow_control)
+
+ # Send compressed 'Hello' on logical channel 1 and 2.
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message=compressed_hello,
+ rsv1=True))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message=compressed_hello,
+ rsv1=True))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual(['Hello'], dispatcher.channel_events[1].messages)
+ self.assertEqual(['Hello'], dispatcher.channel_events[2].messages)
+ # Written 'Hello's should be compressed.
+ messages = request.connection.get_written_messages(1)
+ self.assertEqual(1, len(messages))
+ self.assertEqual(compressed_hello, messages[0])
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(1, len(messages))
+ self.assertEqual(compressed_hello, messages[0])
+
+ def test_add_channel_delta_encoding_remove_extensions(self):
+ # Enable permessage compress extension on the implicitly opened channel.
+ extensions = common.parse_extensions(
+ '%s; method=deflate' % common.PERMESSAGE_COMPRESSION_EXTENSION)
+ request = _create_mock_request(
+ logical_channel_extensions=extensions)
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ # Remove permessage compress extension.
+ delta = ('GET /echo HTTP/1.1\r\n'
+ 'Sec-WebSocket-Extensions:\r\n'
+ '\r\n')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=1, encoded_handshake=delta)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=20)
+ request.connection.put_bytes(flow_control)
+
+ # Send compressed message on logical channel 2. The message should
+ # be rejected (since rsv1 is set).
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message=compressed_hello,
+ rsv1=True))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_NORMAL_CLOSURE, drop_channel.drop_code)
+ self.assertEqual(2, drop_channel.channel_id)
+ # UnsupportedFrameException should be raised on logical channel 2.
+ self.assertTrue(isinstance(dispatcher.channel_events[2].exception,
+ UnsupportedFrameException))
+
+ def test_add_channel_invalid_encoding(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=3,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_UNKNOWN_REQUEST_ENCODING,
+ drop_channel.drop_code)
+ self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR,
+ request.connection.server_close_code)
+
+ def test_add_channel_incomplete_handshake(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ incomplete_encoded_handshake = 'GET /echo HTTP/1.1'
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=incomplete_encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertTrue(1 in dispatcher.channel_events)
+ self.assertTrue(not 2 in dispatcher.channel_events)
+
+ def test_add_channel_duplicate_channel_id(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_CHANNEL_ALREADY_EXISTS,
+ drop_channel.drop_code)
+ self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR,
+ request.connection.server_close_code)
+
+ def test_receive_drop_channel(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ drop_channel = _create_drop_channel_frame(channel_id=2)
+ request.connection.put_bytes(drop_channel)
+
+ # Terminate implicitly opened channel.
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ exception = dispatcher.channel_events[2].exception
+ self.assertTrue(exception.__class__ == ConnectionTerminatedException)
+
+ def test_receive_ping_frame(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=13)
+ request.connection.put_bytes(flow_control)
+
+ ping_frame = _create_logical_frame(channel_id=2,
+ message='Hello World!',
+ opcode=common.OPCODE_PING)
+ request.connection.put_bytes(ping_frame)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PONG, messages[0]['opcode'])
+ self.assertEqual('Hello World!', messages[0]['message'])
+
+ def test_receive_fragmented_ping(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=13)
+ request.connection.put_bytes(flow_control)
+
+ # Send a ping with message 'Hello world!' in two fragmented frames.
+ ping_frame1 = _create_logical_frame(channel_id=2,
+ message='Hello ',
+ fin=False,
+ opcode=common.OPCODE_PING)
+ request.connection.put_bytes(ping_frame1)
+ ping_frame2 = _create_logical_frame(channel_id=2,
+ message='World!',
+ fin=True,
+ opcode=common.OPCODE_CONTINUATION)
+ request.connection.put_bytes(ping_frame2)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PONG, messages[0]['opcode'])
+ self.assertEqual('Hello World!', messages[0]['message'])
+
+ def test_receive_fragmented_ping_while_receiving_fragmented_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=19)
+ request.connection.put_bytes(flow_control)
+
+ # Send a fragmented frame of message 'Hello '.
+ hello = _create_logical_frame(channel_id=2,
+ message='Hello ',
+ fin=False)
+ request.connection.put_bytes(hello)
+
+ # Before sending the last fragmented frame of the message, send a
+ # fragmented ping.
+ ping1 = _create_logical_frame(channel_id=2,
+ message='Pi',
+ fin=False,
+ opcode=common.OPCODE_PING)
+ request.connection.put_bytes(ping1)
+ ping2 = _create_logical_frame(channel_id=2,
+ message='ng!',
+ fin=True,
+ opcode=common.OPCODE_CONTINUATION)
+ request.connection.put_bytes(ping2)
+
+ # Send the last fragmented frame of the message.
+ world = _create_logical_frame(channel_id=2,
+ message='World!',
+ fin=True,
+ opcode=common.OPCODE_CONTINUATION)
+ request.connection.put_bytes(world)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(['Hello World!'], messages)
+ control_messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PONG, control_messages[0]['opcode'])
+ self.assertEqual('Ping!', control_messages[0]['message'])
+
+ def test_receive_two_ping_while_receiving_fragmented_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=25)
+ request.connection.put_bytes(flow_control)
+
+ # Send a fragmented frame of message 'Hello '.
+ hello = _create_logical_frame(channel_id=2,
+ message='Hello ',
+ fin=False)
+ request.connection.put_bytes(hello)
+
+ # Before sending the last fragmented frame of the message, send a
+ # fragmented ping and a non-fragmented ping.
+ ping1 = _create_logical_frame(channel_id=2,
+ message='Pi',
+ fin=False,
+ opcode=common.OPCODE_PING)
+ request.connection.put_bytes(ping1)
+ ping2 = _create_logical_frame(channel_id=2,
+ message='ng!',
+ fin=True,
+ opcode=common.OPCODE_CONTINUATION)
+ request.connection.put_bytes(ping2)
+ ping3 = _create_logical_frame(channel_id=2,
+ message='Pong!',
+ fin=True,
+ opcode=common.OPCODE_PING)
+ request.connection.put_bytes(ping3)
+
+ # Send the last fragmented frame of the message.
+ world = _create_logical_frame(channel_id=2,
+ message='World!',
+ fin=True,
+ opcode=common.OPCODE_CONTINUATION)
+ request.connection.put_bytes(world)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(['Hello World!'], messages)
+ control_messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PONG, control_messages[0]['opcode'])
+ self.assertEqual('Ping!', control_messages[0]['message'])
+ self.assertEqual(common.OPCODE_PONG, control_messages[1]['opcode'])
+ self.assertEqual('Pong!', control_messages[1]['message'])
+
+ def test_receive_message_while_receiving_fragmented_ping(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=19)
+ request.connection.put_bytes(flow_control)
+
+ # Send a fragmented ping.
+ ping1 = _create_logical_frame(channel_id=2,
+ message='Pi',
+ fin=False,
+ opcode=common.OPCODE_PING)
+ request.connection.put_bytes(ping1)
+
+ # Before sending the last fragmented ping, send a message.
+ # The logical channel (2) should be dropped.
+ message = _create_logical_frame(channel_id=2,
+ message='Hello world!',
+ fin=True)
+ request.connection.put_bytes(message)
+
+ # Send the last fragmented frame of the message.
+ ping2 = _create_logical_frame(channel_id=2,
+ message='ng!',
+ fin=True,
+ opcode=common.OPCODE_CONTINUATION)
+ request.connection.put_bytes(ping2)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(2, drop_channel.channel_id)
+ # No message should be sent on channel 2.
+ self.assertRaises(KeyError,
+ request.connection.get_written_messages,
+ 2)
+ self.assertRaises(KeyError,
+ request.connection.get_written_control_messages,
+ 2)
+
+ def test_send_ping(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/ping')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PING, messages[0]['opcode'])
+ self.assertEqual('Ping!', messages[0]['message'])
+
+ def test_send_fragmented_ping(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/ping')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # Replenish 3 bytes. This isn't enough to send the whole ping frame
+ # because the frame will have 5 bytes message('Ping!'). The frame
+ # should be fragmented.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=3)
+ request.connection.put_bytes(flow_control)
+
+ # Wait until the worker is blocked due to send quota shortage.
+ time.sleep(1)
+
+ # Replenish remaining 2 + 1 bytes (including extra cost).
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=3)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PING, messages[0]['opcode'])
+ self.assertEqual('Ping!', messages[0]['message'])
+
+ def test_send_fragmented_ping_while_sending_fragmented_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(
+ path='/ping_while_hello_world')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # Application will send:
+ # - text message 'Hello ' with fin=0
+ # - ping with 'Ping!' message
+ # - text message 'World!' with fin=1
+ # Replenish (6 + 1) + (2 + 1) bytes so that the ping will be
+ # fragmented on the logical channel.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=10)
+ request.connection.put_bytes(flow_control)
+
+ time.sleep(1)
+
+ # Replenish remaining 3 + 6 bytes.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=9)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(['Hello World!'], messages)
+ control_messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PING, control_messages[0]['opcode'])
+ self.assertEqual('Ping!', control_messages[0]['message'])
+
+ def test_send_fragmented_two_ping_while_sending_fragmented_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(
+ path='/two_ping_while_hello_world')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # Application will send:
+ # - text message 'Hello ' with fin=0
+ # - ping with 'Ping!' message
+ # - ping with 'Pong!' message
+ # - text message 'World!' with fin=1
+ # Replenish (6 + 1) + (2 + 1) bytes so that the first ping will be
+ # fragmented on the logical channel.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=10)
+ request.connection.put_bytes(flow_control)
+
+ time.sleep(1)
+
+ # Replenish remaining 3 + (5 + 1) + 6 bytes. The second ping won't
+ # be fragmented on the logical channel.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=15)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(['Hello World!'], messages)
+ control_messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PING, control_messages[0]['opcode'])
+ self.assertEqual('Ping!', control_messages[0]['message'])
+ self.assertEqual(common.OPCODE_PING, control_messages[1]['opcode'])
+ self.assertEqual('Pong!', control_messages[1]['message'])
+
+ def test_send_drop_channel(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+
+ # DropChannel for channel id 1 which doesn't have reason.
+ frame = create_binary_frame('\x00\x60\x01\x00', mask=True)
+ request.connection.put_bytes(frame)
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_ACKNOWLEDGED,
+ drop_channel.drop_code)
+ self.assertEqual(1, drop_channel.channel_id)
+
+ def test_two_flow_control(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # Replenish 5 bytes.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=5)
+ request.connection.put_bytes(flow_control)
+
+ # Send 10 bytes. The server will try echo back 10 bytes.
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='HelloWorld'))
+
+ # Replenish 5 + 1 (per-message extra cost) bytes.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(['HelloWorld'], messages)
+ received_flow_controls = [
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_FLOW_CONTROL and b.channel_id == 2]
+ # Replenishment for 'HelloWorld' + 1
+ self.assertEqual(11, received_flow_controls[0].send_quota)
+ # Replenishment for 'Goodbye' + 1
+ self.assertEqual(8, received_flow_controls[1].send_quota)
+
+ def test_no_send_quota_on_server(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='HelloWorld'))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ # Just wait for 1 sec so that the server attempts to echo back
+ # 'HelloWorld'.
+ self.assertFalse(mux_handler.wait_until_done(timeout=1))
+
+ # No message should be sent on channel 2.
+ self.assertRaises(KeyError,
+ request.connection.get_written_messages,
+ 2)
+
+ def test_no_send_quota_on_server_for_permessage_extra_cost(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Hello'))
+ # Replenish only len('World') bytes.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=5)
+ request.connection.put_bytes(flow_control)
+ # Server should not callback for this message.
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='World'))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ # Just wait for 1 sec so that the server attempts to echo back
+ # 'World'.
+ self.assertFalse(mux_handler.wait_until_done(timeout=1))
+
+ # Only one message should be sent on channel 2.
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(['Hello'], messages)
+
+ def test_quota_violation_by_client(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, 0)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='HelloWorld'))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ control_blocks = request.connection.get_written_control_blocks()
+ self.assertEqual(5, len(control_blocks))
+ drop_channel = next(
+ b for b in control_blocks
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_SEND_QUOTA_VIOLATION,
+ drop_channel.drop_code)
+
+ def test_consume_quota_empty_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ # Client has 1 byte quota.
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, 1)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=2)
+ request.connection.put_bytes(flow_control)
+ # Send an empty message. Pywebsocket always replenishes 1 byte quota
+ # for empty message
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message=''))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ # This message violates quota on channel id 2.
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual(1, len(dispatcher.channel_events[2].messages))
+ self.assertEqual('', dispatcher.channel_events[2].messages[0])
+
+ received_flow_controls = [
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_FLOW_CONTROL and b.channel_id == 2]
+ self.assertEqual(1, len(received_flow_controls))
+ self.assertEqual(1, received_flow_controls[0].send_quota)
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(2, drop_channel.channel_id)
+ self.assertEqual(mux._DROP_CODE_SEND_QUOTA_VIOLATION,
+ drop_channel.drop_code)
+
+ def test_consume_quota_fragmented_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ # Client has len('Hello') + len('Goodbye') + 2 bytes quota.
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, 14)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='He', fin=False,
+ opcode=common.OPCODE_TEXT))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='llo', fin=True,
+ opcode=common.OPCODE_CONTINUATION))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(['Hello'], messages)
+
+ def test_fragmented_control_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/ping')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # Replenish total 6 bytes in 3 FlowControls.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=1)
+ request.connection.put_bytes(flow_control)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=2)
+ request.connection.put_bytes(flow_control)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=3)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PING, messages[0]['opcode'])
+ self.assertEqual('Ping!', messages[0]['message'])
+
+ def test_channel_slot_violation_by_client(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(slots=1,
+ send_quota=mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Hello'))
+
+ # This request should be rejected.
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=3, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+ flow_control = _create_flow_control_frame(channel_id=3,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=3, message='Hello'))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual([], dispatcher.channel_events[1].messages)
+ self.assertEqual(['Hello'], dispatcher.channel_events[2].messages)
+ self.assertFalse(dispatcher.channel_events.has_key(3))
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(3, drop_channel.channel_id)
+ self.assertEqual(mux._DROP_CODE_NEW_CHANNEL_SLOT_VIOLATION,
+ drop_channel.drop_code)
+
+ def test_quota_overflow_by_client(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(slots=1,
+ send_quota=mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+ # Replenish 0x7FFFFFFFFFFFFFFF bytes twice.
+ flow_control = _create_flow_control_frame(
+ channel_id=2,
+ replenished_quota=0x7FFFFFFFFFFFFFFF)
+ request.connection.put_bytes(flow_control)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(2, drop_channel.channel_id)
+ self.assertEqual(mux._DROP_CODE_SEND_QUOTA_OVERFLOW,
+ drop_channel.drop_code)
+
+ def test_invalid_encapsulated_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+
+ first_byte = (mux._MUX_OPCODE_ADD_CHANNEL_REQUEST << 5)
+ block = (chr(first_byte) +
+ mux._encode_channel_id(1) +
+ mux._encode_number(0))
+ payload = mux._encode_channel_id(mux._CONTROL_CHANNEL_ID) + block
+ text_frame = create_binary_frame(payload, opcode=common.OPCODE_TEXT,
+ mask=True)
+ request.connection.put_bytes(text_frame)
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_INVALID_ENCAPSULATING_MESSAGE,
+ drop_channel.drop_code)
+ self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR,
+ request.connection.server_close_code)
+
+ def test_channel_id_truncated(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+
+ # The last byte of the channel id is missing.
+ frame = create_binary_frame('\x80', mask=True)
+ request.connection.put_bytes(frame)
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_CHANNEL_ID_TRUNCATED,
+ drop_channel.drop_code)
+ self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR,
+ request.connection.server_close_code)
+
+ def test_inner_frame_truncated(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+
+ # Just contain channel id 1.
+ frame = create_binary_frame('\x01', mask=True)
+ request.connection.put_bytes(frame)
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_ENCAPSULATED_FRAME_IS_TRUNCATED,
+ drop_channel.drop_code)
+ self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR,
+ request.connection.server_close_code)
+
+ def test_unknown_mux_opcode(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+
+ # Undefined opcode 5
+ frame = create_binary_frame('\x00\xa0', mask=True)
+ request.connection.put_bytes(frame)
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_UNKNOWN_MUX_OPCODE,
+ drop_channel.drop_code)
+ self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR,
+ request.connection.server_close_code)
+
+ def test_invalid_mux_control_block(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+
+ # DropChannel contains 1 byte reason
+ frame = create_binary_frame('\x00\x60\x00\x01\x00', mask=True)
+ request.connection.put_bytes(frame)
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+ drop_channel.drop_code)
+ self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR,
+ request.connection.server_close_code)
+
+ def test_permessage_compress(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ # Enable permessage compress extension on logical channel 2.
+ extensions = '%s; method=deflate' % (
+ common.PERMESSAGE_COMPRESSION_EXTENSION)
+ encoded_handshake = _create_request_header(path='/echo',
+ extensions=extensions)
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=20)
+ request.connection.put_bytes(flow_control)
+
+ # Send compressed 'Hello' twice.
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello1 = compress.compress('Hello')
+ compressed_hello1 += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello1 = compressed_hello1[:-4]
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message=compressed_hello1,
+ rsv1=True))
+ compressed_hello2 = compress.compress('Hello')
+ compressed_hello2 += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello2 = compressed_hello2[:-4]
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message=compressed_hello2,
+ rsv1=True))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual(['Hello', 'Hello'],
+ dispatcher.channel_events[2].messages)
+ # Written 'Hello's should be compressed.
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(2, len(messages))
+ self.assertEqual(compressed_hello1, messages[0])
+ self.assertEqual(compressed_hello2, messages[1])
+
+
+ def test_permessage_compress_fragmented_message(self):
+ extensions = common.parse_extensions(
+ '%s; method=deflate' % common.PERMESSAGE_COMPRESSION_EXTENSION)
+ request = _create_mock_request(
+ logical_channel_extensions=extensions)
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ # Send compressed 'HelloHelloHello' as fragmented message.
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('HelloHelloHello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+
+ m = len(compressed_hello) / 2
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1,
+ message=compressed_hello[:m],
+ fin=False, rsv1=True,
+ opcode=common.OPCODE_TEXT))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1,
+ message=compressed_hello[m:],
+ fin=True, rsv1=False,
+ opcode=common.OPCODE_CONTINUATION))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual(['HelloHelloHello'],
+ dispatcher.channel_events[1].messages)
+ messages = request.connection.get_written_messages(1)
+ self.assertEqual(1, len(messages))
+ self.assertEqual(compressed_hello, messages[0])
+
+ def test_receive_bad_fragmented_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # Send a frame with fin=False, and then send a frame with
+ # opcode=TEXT (not CONTINUATION). Logical channel 2 should be dropped.
+ frame1 = _create_logical_frame(channel_id=2,
+ message='Hello ',
+ fin=False,
+ opcode=common.OPCODE_TEXT)
+ request.connection.put_bytes(frame1)
+ frame2 = _create_logical_frame(channel_id=2,
+ message='World!',
+ fin=True,
+ opcode=common.OPCODE_TEXT)
+ request.connection.put_bytes(frame2)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=3, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # Send a frame with opcode=CONTINUATION without a preceding frame
+ # the fin of which is not set. Logical channel 3 should be dropped.
+ frame3 = _create_logical_frame(channel_id=3,
+ message='Hello',
+ fin=True,
+ opcode=common.OPCODE_CONTINUATION)
+ request.connection.put_bytes(frame3)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=4, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # Send a frame with opcode=PING and fin=False, and then send a frame
+ # with opcode=TEXT (not CONTINUATION). Logical channel 4 should be
+ # dropped.
+ frame4 = _create_logical_frame(channel_id=4,
+ message='Ping',
+ fin=False,
+ opcode=common.OPCODE_PING)
+ request.connection.put_bytes(frame4)
+ frame5 = _create_logical_frame(channel_id=4,
+ message='Hello',
+ fin=True,
+ opcode=common.OPCODE_TEXT)
+ request.connection.put_bytes(frame5)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channels = [
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL]
+ self.assertEqual(3, len(drop_channels))
+ for d in drop_channels:
+ self.assertEqual(mux._DROP_CODE_BAD_FRAGMENTATION,
+ d.drop_code)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream.py
new file mode 100755
index 000000000..81acfeb04
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for stream module."""
+
+
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import common
+from mod_pywebsocket import stream
+
+
+class StreamTest(unittest.TestCase):
+ """A unittest for stream module."""
+
+ def test_create_header(self):
+ # more, rsv1, ..., rsv4 are all true
+ header = stream.create_header(common.OPCODE_TEXT, 1, 1, 1, 1, 1, 1)
+ self.assertEqual('\xf1\x81', header)
+
+ # Maximum payload size
+ header = stream.create_header(
+ common.OPCODE_TEXT, (1 << 63) - 1, 0, 0, 0, 0, 0)
+ self.assertEqual('\x01\x7f\x7f\xff\xff\xff\xff\xff\xff\xff', header)
+
+ # Invalid opcode 0x10
+ self.assertRaises(ValueError,
+ stream.create_header,
+ 0x10, 0, 0, 0, 0, 0, 0)
+
+ # Invalid value 0xf passed to more parameter
+ self.assertRaises(ValueError,
+ stream.create_header,
+ common.OPCODE_TEXT, 0, 0xf, 0, 0, 0, 0)
+
+ # Too long payload_length
+ self.assertRaises(ValueError,
+ stream.create_header,
+ common.OPCODE_TEXT, 1 << 63, 0, 0, 0, 0, 0)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream_hixie75.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream_hixie75.py
new file mode 100755
index 000000000..ca9ac7130
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream_hixie75.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for stream module."""
+
+
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket.stream import StreamHixie75
+from test.test_msgutil import _create_request_hixie75
+
+
+class StreamHixie75Test(unittest.TestCase):
+ """A unittest for StreamHixie75 class."""
+
+ def test_payload_length(self):
+ for length, bytes in ((0, '\x00'), (0x7f, '\x7f'), (0x80, '\x81\x00'),
+ (0x1234, '\x80\xa4\x34')):
+ test_stream = StreamHixie75(_create_request_hixie75(bytes))
+ self.assertEqual(
+ length, test_stream._read_payload_length_hixie75())
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_util.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_util.py
new file mode 100755
index 000000000..20f4ab059
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_util.py
@@ -0,0 +1,200 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for util module."""
+
+
+import os
+import random
+import sys
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import util
+
+
+_TEST_DATA_DIR = os.path.join(os.path.split(__file__)[0], 'testdata')
+
+
+class UtilTest(unittest.TestCase):
+ """A unittest for util module."""
+
+ def test_get_stack_trace(self):
+ self.assertEqual('None\n', util.get_stack_trace())
+ try:
+ a = 1 / 0 # Intentionally raise exception.
+ except Exception:
+ trace = util.get_stack_trace()
+ self.failUnless(trace.startswith('Traceback'))
+ self.failUnless(trace.find('ZeroDivisionError') != -1)
+
+ def test_prepend_message_to_exception(self):
+ exc = Exception('World')
+ self.assertEqual('World', str(exc))
+ util.prepend_message_to_exception('Hello ', exc)
+ self.assertEqual('Hello World', str(exc))
+
+ def test_get_script_interp(self):
+ cygwin_path = 'c:\\cygwin\\bin'
+ cygwin_perl = os.path.join(cygwin_path, 'perl')
+ self.assertEqual(None, util.get_script_interp(
+ os.path.join(_TEST_DATA_DIR, 'README')))
+ self.assertEqual(None, util.get_script_interp(
+ os.path.join(_TEST_DATA_DIR, 'README'), cygwin_path))
+ self.assertEqual('/usr/bin/perl -wT', util.get_script_interp(
+ os.path.join(_TEST_DATA_DIR, 'hello.pl')))
+ self.assertEqual(cygwin_perl + ' -wT', util.get_script_interp(
+ os.path.join(_TEST_DATA_DIR, 'hello.pl'), cygwin_path))
+
+ def test_hexify(self):
+ self.assertEqual('61 7a 41 5a 30 39 20 09 0d 0a 00 ff',
+ util.hexify('azAZ09 \t\r\n\x00\xff'))
+
+
+class RepeatedXorMaskerTest(unittest.TestCase):
+ """A unittest for RepeatedXorMasker class."""
+
+ def test_mask(self):
+ # Sample input e6,97,a5 is U+65e5 in UTF-8
+ masker = util.RepeatedXorMasker('\xff\xff\xff\xff')
+ result = masker.mask('\xe6\x97\xa5')
+ self.assertEqual('\x19\x68\x5a', result)
+
+ masker = util.RepeatedXorMasker('\x00\x00\x00\x00')
+ result = masker.mask('\xe6\x97\xa5')
+ self.assertEqual('\xe6\x97\xa5', result)
+
+ masker = util.RepeatedXorMasker('\xe6\x97\xa5\x20')
+ result = masker.mask('\xe6\x97\xa5')
+ self.assertEqual('\x00\x00\x00', result)
+
+ def test_mask_twice(self):
+ masker = util.RepeatedXorMasker('\x00\x7f\xff\x20')
+ # mask[0], mask[1], ... will be used.
+ result = masker.mask('\x00\x00\x00\x00\x00')
+ self.assertEqual('\x00\x7f\xff\x20\x00', result)
+ # mask[2], mask[0], ... will be used for the next call.
+ result = masker.mask('\x00\x00\x00\x00\x00')
+ self.assertEqual('\x7f\xff\x20\x00\x7f', result)
+
+ def test_mask_large_data(self):
+ masker = util.RepeatedXorMasker('mASk')
+ original = ''.join([chr(i % 256) for i in xrange(1000)])
+ result = masker.mask(original)
+ expected = ''.join(
+ [chr((i % 256) ^ ord('mASk'[i % 4])) for i in xrange(1000)])
+ self.assertEqual(expected, result)
+
+ masker = util.RepeatedXorMasker('MaSk')
+ first_part = 'The WebSocket Protocol enables two-way communication.'
+ result = masker.mask(first_part)
+ self.assertEqual(
+ '\x19\t6K\x1a\x0418"\x028\x0e9A\x03\x19"\x15<\x08"\rs\x0e#'
+ '\x001\x07(\x12s\x1f:\x0e~\x1c,\x18s\x08"\x0c>\x1e#\x080\n9'
+ '\x08<\x05c',
+ result)
+ second_part = 'It has two parts: a handshake and the data transfer.'
+ result = masker.mask(second_part)
+ self.assertEqual(
+ "('K%\x00 K9\x16<K=\x00!\x1f>[s\nm\t2\x05)\x12;\n&\x04s\n#"
+ "\x05s\x1f%\x04s\x0f,\x152K9\x132\x05>\x076\x19c",
+ result)
+
+
+def get_random_section(source, min_num_chunks):
+ chunks = []
+ bytes_chunked = 0
+
+ while bytes_chunked < len(source):
+ chunk_size = random.randint(
+ 1,
+ min(len(source) / min_num_chunks, len(source) - bytes_chunked))
+ chunk = source[bytes_chunked:bytes_chunked + chunk_size]
+ chunks.append(chunk)
+ bytes_chunked += chunk_size
+
+ return chunks
+
+
+class InflaterDeflaterTest(unittest.TestCase):
+ """A unittest for _Inflater and _Deflater class."""
+
+ def test_inflate_deflate_default(self):
+ input = b'hello' + '-' * 30000 + b'hello'
+ inflater15 = util._Inflater(15)
+ deflater15 = util._Deflater(15)
+ inflater8 = util._Inflater(8)
+ deflater8 = util._Deflater(8)
+
+ compressed15 = deflater15.compress_and_finish(input)
+ compressed8 = deflater8.compress_and_finish(input)
+
+ inflater15.append(compressed15)
+ inflater8.append(compressed8)
+
+ self.assertNotEqual(compressed15, compressed8)
+ self.assertEqual(input, inflater15.decompress(-1))
+ self.assertEqual(input, inflater8.decompress(-1))
+
+ def test_random_section(self):
+ random.seed(a=0)
+ source = ''.join(
+ [chr(random.randint(0, 255)) for i in xrange(100 * 1024)])
+
+ chunked_input = get_random_section(source, 10)
+ print "Input chunk sizes: %r" % [len(c) for c in chunked_input]
+
+ deflater = util._Deflater(15)
+ compressed = []
+ for chunk in chunked_input:
+ compressed.append(deflater.compress(chunk))
+ compressed.append(deflater.compress_and_finish(''))
+
+ chunked_expectation = get_random_section(source, 10)
+ print ("Expectation chunk sizes: %r" %
+ [len(c) for c in chunked_expectation])
+
+ inflater = util._Inflater(15)
+ inflater.append(''.join(compressed))
+ for chunk in chunked_expectation:
+ decompressed = inflater.decompress(len(chunk))
+ self.assertEqual(chunk, decompressed)
+
+ self.assertEqual('', inflater.decompress(-1))
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/README b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/README
new file mode 100644
index 000000000..c001aa559
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/README
@@ -0,0 +1 @@
+Test data directory
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/abort_by_user_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/abort_by_user_wsh.py
new file mode 100644
index 000000000..367f9930f
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/abort_by_user_wsh.py
@@ -0,0 +1,42 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+from mod_pywebsocket import handshake
+
+
+def web_socket_do_extra_handshake(request):
+ raise handshake.AbortedByUserException("abort for test")
+
+
+def web_socket_transfer_data(request):
+ raise handshake.AbortedByUserException("abort for test")
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/blank_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/blank_wsh.py
new file mode 100644
index 000000000..7f87c6af2
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/blank_wsh.py
@@ -0,0 +1,31 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+# intentionally left blank
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/origin_check_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/origin_check_wsh.py
new file mode 100644
index 000000000..2c139fa17
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/origin_check_wsh.py
@@ -0,0 +1,42 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+def web_socket_do_extra_handshake(request):
+ if request.ws_origin == 'http://example.com':
+ return
+ raise ValueError('Unacceptable origin: %r' % request.ws_origin)
+
+
+def web_socket_transfer_data(request):
+ request.connection.write('origin_check_wsh.py is called for %s, %s' %
+ (request.ws_resource, request.ws_protocol))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/exception_in_transfer_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/exception_in_transfer_wsh.py
new file mode 100644
index 000000000..b982d0231
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/exception_in_transfer_wsh.py
@@ -0,0 +1,44 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Exception in web_socket_transfer_data().
+"""
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ raise Exception('Intentional Exception for %s, %s' %
+ (request.ws_resource, request.ws_protocol))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/no_wsh_at_the_end.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/no_wsh_at_the_end.py
new file mode 100644
index 000000000..17e7be180
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/no_wsh_at_the_end.py
@@ -0,0 +1,45 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Correct signatures, wrong file name.
+"""
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ request.connection.write(
+ 'sub/no_wsh_at_the_end.py is called for %s, %s' %
+ (request.ws_resource, request.ws_protocol))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/non_callable_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/non_callable_wsh.py
new file mode 100644
index 000000000..26352eb4c
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/non_callable_wsh.py
@@ -0,0 +1,39 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Non-callable handlers.
+"""
+
+
+web_socket_do_extra_handshake = True
+web_socket_transfer_data = 1
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/plain_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/plain_wsh.py
new file mode 100644
index 000000000..db3ff6930
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/plain_wsh.py
@@ -0,0 +1,40 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ request.connection.write('sub/plain_wsh.py is called for %s, %s' %
+ (request.ws_resource, request.ws_protocol))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py
new file mode 100644
index 000000000..6bf659bc9
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py
@@ -0,0 +1,45 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Wrong web_socket_do_extra_handshake signature.
+"""
+
+
+def no_web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ request.connection.write(
+ 'sub/wrong_handshake_sig_wsh.py is called for %s, %s' %
+ (request.ws_resource, request.ws_protocol))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py
new file mode 100644
index 000000000..e0e2e5507
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py
@@ -0,0 +1,45 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Wrong web_socket_transfer_data() signature.
+"""
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def no_web_socket_transfer_data(request):
+ request.connection.write(
+ 'sub/wrong_transfer_sig_wsh.py is called for %s, %s' %
+ (request.ws_resource, request.ws_protocol))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/hello.pl b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/hello.pl
new file mode 100644
index 000000000..882ef5a10
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/hello.pl
@@ -0,0 +1,32 @@
+#!/usr/bin/perl -wT
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+print "Hello\n";