diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/tools/third_party/pywebsocket3 | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/tools/third_party/pywebsocket3')
80 files changed, 13147 insertions, 0 deletions
diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/.gitignore b/testing/web-platform/tests/tools/third_party/pywebsocket3/.gitignore new file mode 100644 index 0000000000..70f2867054 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/.gitignore @@ -0,0 +1,4 @@ +*.pyc +build/ +*.egg-info/ +dist/ diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/.travis.yml b/testing/web-platform/tests/tools/third_party/pywebsocket3/.travis.yml new file mode 100644 index 0000000000..2065a644dd --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/.travis.yml @@ -0,0 +1,17 @@ +language: python +python: + - 2.7 + - 3.5 + - 3.6 + - 3.7 + - 3.8 + - nightly + +matrix: + allow_failures: + - python: 3.5, nightly +install: + - pip install six yapf +script: + - python test/run_all.py + - yapf --diff --recursive . diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/CONTRIBUTING b/testing/web-platform/tests/tools/third_party/pywebsocket3/CONTRIBUTING new file mode 100644 index 0000000000..f975be126f --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/CONTRIBUTING @@ -0,0 +1,30 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution; +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to <https://cla.developers.google.com/> to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. +For instructions for contributing code, please read: +https://github.com/google/pywebsocket/wiki/CodeReviewInstruction + +## Community Guidelines + +This project follows +[Google's Open Source Community Guidelines](https://opensource.google/conduct/). diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/LICENSE b/testing/web-platform/tests/tools/third_party/pywebsocket3/LICENSE new file mode 100644 index 0000000000..c91bea9025 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/LICENSE @@ -0,0 +1,28 @@ +Copyright 2020, 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/third_party/pywebsocket3/MANIFEST.in b/testing/web-platform/tests/tools/third_party/pywebsocket3/MANIFEST.in new file mode 100644 index 0000000000..19256882c5 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/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/third_party/pywebsocket3/README.md b/testing/web-platform/tests/tools/third_party/pywebsocket3/README.md new file mode 100644 index 0000000000..8684f2cc7e --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/README.md @@ -0,0 +1,36 @@ + +# pywebsocket3 # + +The pywebsocket project aims to provide a [WebSocket](https://tools.ietf.org/html/rfc6455) standalone server. + +pywebsocket is intended for **testing** or **experimental** purposes. + +Run this to read the general document: +``` +$ pydoc mod_pywebsocket +``` + +Please see [Wiki](https://github.com/GoogleChromeLabs/pywebsocket3/wiki) for more details. + +# 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 standalone server, run this to read the document: +``` +$ pydoc mod_pywebsocket.standalone +``` +# Disclaimer # +This is not an officially supported Google product diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_handshake_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_handshake_wsh.py new file mode 100644 index 0000000000..1b719ca897 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/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 __future__ import absolute_import +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/third_party/pywebsocket3/example/abort_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/abort_wsh.py new file mode 100644 index 0000000000..d4c240bf2c --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/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 __future__ import absolute_import +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/third_party/pywebsocket3/example/arraybuffer_benchmark.html b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/arraybuffer_benchmark.html new file mode 100644 index 0000000000..869cd7e1ee --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/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/third_party/pywebsocket3/example/bench_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/bench_wsh.py new file mode 100644 index 0000000000..2df50e77db --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/bench_wsh.py @@ -0,0 +1,59 @@ +# 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. +""" + +from __future__ import absolute_import +import time +from six.moves import range + + +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 range(count): + request.ws_stream.send_message(message) + time.sleep(wait) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.html b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.html new file mode 100644 index 0000000000..f1e5c97b3a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.html @@ -0,0 +1,175 @@ +<!-- +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: getFloatArrayFromInput('multipliers'), + verifyData: getBoolFromCheckBox('verifydata'), + addToLog: addToLog, + addToSummary: addToSummary, + measureValue: measureValue, + notifyAbort: notifyAbort + }; +} + +function onSendBenchmark() { + var config = getConfig(); + doAction(config, getBoolFromCheckBox('worker'), 'sendBenchmark'); +} + +function onReceiveBenchmark() { + var config = getConfig(); + doAction(config, getBoolFromCheckBox('worker'), 'receiveBenchmark'); +} + +function onBatchBenchmark() { + var config = getConfig(); + doAction(config, getBoolFromCheckBox('worker'), 'batchBenchmark'); +} + +function onStop() { + var config = getConfig(); + doAction(config, getBoolFromCheckBox('worker'), 'stop'); +} + +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'); + } + + initWorker(''); +} +</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/third_party/pywebsocket3/example/benchmark.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.js new file mode 100644 index 0000000000..2701472a4f --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark.js @@ -0,0 +1,238 @@ +// Copyright 2014, 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. + + +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, isWarmUp) { + timerID = null; + + var totalSize = 0; + var totalReplied = 0; + + var onMessageHandler = function(event) { + if (!verifyAcknowledgement(config, event.data, size)) { + destroyAllSockets(); + config.notifyAbort(); + return; + } + + totalReplied += size; + + if (totalReplied < totalSize) { + return; + } + + calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize, + isWarmUp); + + 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, isWarmUp) { + 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(); + config.notifyAbort(); + return; + } + + if (config.verifyData && !verifyArrayBuffer(event.data, 0x61)) { + config.addToLog('Response verification failed'); + destroyAllSockets(); + config.notifyAbort(); + return; + } + + totalReplied += bytesReceived; + + if (totalReplied < totalSize) { + return; + } + + calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize, + isWarmUp); + + 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'); + config.notifyAbort(); + }; + return socket; +} + +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 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 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 cleanup() { + destroyAllSockets(); +} diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark_helper_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark_helper_wsh.py new file mode 100644 index 0000000000..fc17533335 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/benchmark_helper_wsh.py @@ -0,0 +1,84 @@ +# 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.""" +from __future__ import absolute_import +import six + + +def web_socket_do_extra_handshake(request): + # Turn off compression. + request.ws_extension_processors = [] + + +def web_socket_transfer_data(request): + data = b'' + + while True: + command = request.ws_stream.receive_message() + if command is None: + return + + if not isinstance(command, six.text_type): + 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 = b'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 != b'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/third_party/pywebsocket3/example/cgi-bin/hi.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cgi-bin/hi.py new file mode 100755 index 0000000000..f136f2c442 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cgi-bin/hi.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +print('Content-Type: text/plain') +print('') +print('Hi from hi.py') diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/close_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/close_wsh.py new file mode 100644 index 0000000000..8f0005ffea --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/close_wsh.py @@ -0,0 +1,70 @@ +# 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 __future__ import absolute_import +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/third_party/pywebsocket3/example/console.html b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/console.html new file mode 100644 index 0000000000..ccd6d8f806 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/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/third_party/pywebsocket3/example/cookie_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cookie_wsh.py new file mode 100644 index 0000000000..815209694e --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/cookie_wsh.py @@ -0,0 +1,54 @@ +# Copyright 2020 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 __future__ import absolute_import +from six.moves import urllib + + +def _add_set_cookie(request, value): + request.extra_headers.append(('Set-Cookie', value)) + + +def web_socket_do_extra_handshake(request): + components = urllib.parse.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/third_party/pywebsocket3/example/echo_client.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_client.py new file mode 100755 index 0000000000..2ed60b3b59 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_client.py @@ -0,0 +1,699 @@ +#!/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 +""" + +from __future__ import absolute_import +from __future__ import print_function +import base64 +import codecs +from hashlib import sha1 +import logging +import argparse +import os +import random +import re +import six +import socket +import ssl +import struct +import sys + +from mod_pywebsocket import common +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 StreamOptions +from mod_pywebsocket import util + +_TIMEOUT_SEC = 10 +_UNDEFINED_PORT = -1 + +_UPGRADE_HEADER = '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' + + +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): + recv_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)) + recv_bytes.append(received_bytes) + remaining -= len(received_bytes) + return b''.join(recv_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): + self._logger = util.get_class_logger(self) + + self._tls_socket = ssl.wrap_socket(raw_socket) + + # Print cipher in use. Handshake is done on wrap_socket call. + self._logger.info("Cipher: %s", self._tls_socket.cipher()) + + 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 = b'' + value = b'' + # 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 != b'\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.decode('UTF-8'), + []).append(value.decode('UTF-8')) + # 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 = b'' + while True: + # 4.1 34. read a byte from the server + ch = _receive_bytes(self._socket, 1) + if ch == b'\r': # 0x0D + return None + elif ch == b'\n': # 0x0A + raise ClientHandshakeError( + 'Unexpected LF when reading header name %r' % name) + elif ch == b':': # 0x3A + return name.lower() + 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 == b' ': # 0x20 + continue + return ch + + def _read_value(self, ch): + # 4.1 33. let /value/ be empty byte arrays + value = b'' + # 4.1 36. read a byte from server. + while True: + if ch == b'\r': # 0x0D + return value + elif ch == b'\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 + """ + 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.encode('UTF-8')) + + 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: + 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.decode('UTF-8'))) + + fields.append( + '%s: %d\r\n' % + (common.SEC_WEBSOCKET_VERSION_HEADER, common.VERSION_HYBI_LATEST)) + + extensions_to_request = [] + + 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.encode('UTF-8')) + + self._socket.sendall(b'\r\n') + + self._logger.debug('Sent client\'s opening handshake headers: %r', + fields) + self._logger.debug('Start reading Status-Line') + + status_line = b'' + while True: + ch = _receive_bytes(self._socket, 1) + status_line += ch + if ch == b'\n': + break + + m = re.match(b'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 != b'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 != b'\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: + 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 = sha1(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.decode('UTF-8'): + raise ClientHandshakeError( + 'Invalid %s header: %r (expected: %s)' % + (common.SEC_WEBSOCKET_ACCEPT_HEADER, accept, expected_accept)) + + 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]) + + for extension in accepted_extensions: + extension_name = extension.name() + if (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.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 ClientConnection(object): + """A wrapper for socket object to provide the mp_conn interface. + """ + 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) + self.ws_version = common.VERSION_HYBI_LATEST + + +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._handshake = ClientHandshakeProcessor(self._socket, + self._options) + + self._handshake.handshake() + + self._logger.info('Connection established') + + request = ClientRequest(self._socket) + + stream_option = StreamOptions() + stream_option.mask_send = True + stream_option.unmask_receive = False + + 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) + + 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 as 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(): + # Force Python 2 to use the locale encoding, even when the output is not a + # tty. This makes the behaviour the same as Python 3. The encoding won't + # necessarily support all unicode characters. This problem is particularly + # prevalent on Windows. + if six.PY2: + import locale + encoding = locale.getpreferredencoding() + sys.stdout = codecs.getwriter(encoding)(sys.stdout) + + parser = argparse.ArgumentParser() + # 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_argument('-s', + '--server-host', + '--server_host', + dest='server_host', + type=six.text_type, + default='localhost', + help='server host') + parser.add_argument('-p', + '--server-port', + '--server_port', + dest='server_port', + type=int, + default=_UNDEFINED_PORT, + help='server port') + parser.add_argument('-o', + '--origin', + dest='origin', + type=six.text_type, + default=None, + help='origin') + parser.add_argument('-r', + '--resource', + dest='resource', + type=six.text_type, + default='/echo', + help='resource path') + parser.add_argument( + '-m', + '--message', + dest='message', + type=six.text_type, + default=u'Hello,<>', + help=('comma-separated messages to send. ' + '%s will force close the connection from server.' % + _GOODBYE_MESSAGE)) + parser.add_argument('-q', + '--quiet', + dest='verbose', + action='store_false', + default=True, + help='suppress messages') + parser.add_argument('-t', + '--tls', + dest='use_tls', + action='store_true', + default=False, + help='use TLS (wss://).') + parser.add_argument('-k', + '--socket-timeout', + '--socket_timeout', + dest='socket_timeout', + type=int, + default=_TIMEOUT_SEC, + help='Timeout(sec) for sockets') + parser.add_argument('--use-permessage-deflate', + '--use_permessage_deflate', + dest='use_permessage_deflate', + action='store_true', + default=False, + help='Use the permessage-deflate extension.') + parser.add_argument('--log-level', + '--log_level', + type=six.text_type, + dest='log_level', + default='warn', + choices=['debug', 'info', 'warn', 'error', 'critical'], + help='Log level.') + + options = parser.parse_args() + + logging.basicConfig(level=logging.getLevelName(options.log_level.upper())) + + # 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 + + EchoClient(options).run() + + +if __name__ == '__main__': + main() + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_noext_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_noext_wsh.py new file mode 100644 index 0000000000..eba5032218 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_noext_wsh.py @@ -0,0 +1,62 @@ +# 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. + +import six + +_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, six.text_type): + 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/third_party/pywebsocket3/example/echo_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_wsh.py new file mode 100644 index 0000000000..f7b3c6c531 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/echo_wsh.py @@ -0,0 +1,55 @@ +# 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. + +import six + +_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, six.text_type): + 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/third_party/pywebsocket3/example/handler_map.txt b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/handler_map.txt new file mode 100644 index 0000000000..21c4c09aa0 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/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/third_party/pywebsocket3/example/hsts_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/hsts_wsh.py new file mode 100644 index 0000000000..e861946921 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/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/third_party/pywebsocket3/example/internal_error_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/internal_error_wsh.py new file mode 100644 index 0000000000..04aa684283 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/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 __future__ import absolute_import +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/third_party/pywebsocket3/example/origin_check_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/origin_check_wsh.py new file mode 100644 index 0000000000..e05767ab93 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/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/third_party/pywebsocket3/example/performance_test_iframe.html b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.html new file mode 100644 index 0000000000..c18b2c08f6 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.html @@ -0,0 +1,37 @@ +<!-- +Copyright 2020, 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. +--> + +<!DOCTYPE html> +<head> +<script src="util.js"></script> +<script src="performance_test_iframe.js"></script> +<script src="benchmark.js"></script> +</head> diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.js new file mode 100644 index 0000000000..270409aa6e --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/performance_test_iframe.js @@ -0,0 +1,86 @@ +// Copyright 2020, 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. + +function perfTestAddToLog(text) { + parent.postMessage({'command': 'log', 'value': text}, '*'); +} + +function perfTestAddToSummary(text) { +} + +function perfTestMeasureValue(value) { + parent.postMessage({'command': 'measureValue', 'value': value}, '*'); +} + +function perfTestNotifyAbort() { + parent.postMessage({'command': 'notifyAbort'}, '*'); +} + +function getConfigForPerformanceTest(dataType, async, + verifyData, numIterations, + numWarmUpIterations) { + + return { + prefixUrl: 'ws://' + location.host + '/benchmark_helper', + printSize: true, + numSockets: 1, + // + 1 is for a warmup iteration by the Telemetry framework. + numIterations: numIterations + numWarmUpIterations + 1, + numWarmUpIterations: numWarmUpIterations, + minTotal: 10240000, + startSize: 10240000, + stopThreshold: 10240000, + multipliers: [2], + verifyData: verifyData, + dataType: dataType, + async: async, + addToLog: perfTestAddToLog, + addToSummary: perfTestAddToSummary, + measureValue: perfTestMeasureValue, + notifyAbort: perfTestNotifyAbort + }; +} + +var data; +onmessage = function(message) { + var action; + if (message.data.command === 'start') { + data = message.data; + initWorker('http://' + location.host); + action = data.benchmarkName; + } else { + action = 'stop'; + } + + var config = getConfigForPerformanceTest(data.dataType, data.async, + data.verifyData, + data.numIterations, + data.numWarmUpIterations); + doAction(config, data.isWorker, action); +}; diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/special_headers.cgi b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/special_headers.cgi new file mode 100755 index 0000000000..703cb7401b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/special_headers.cgi @@ -0,0 +1,26 @@ +#!/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/third_party/pywebsocket3/example/util.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util.js new file mode 100644 index 0000000000..990160cb40 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util.js @@ -0,0 +1,323 @@ +// 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, + isWarmUp) { + var timeSpentInMs = getTimeStamp() - startTimeInMs; + var speed = calculateSpeedInKB(totalSize, timeSpentInMs); + var timePerMessageInMs = timeSpentInMs / (totalSize / size); + if (!isWarmUp) { + config.measureValue(timePerMessageInMs); + if (!results[size]) { + results[size] = {n: 0, sum_t: 0, sum_t2: 0}; + } + results[size].n ++; + results[size].sum_t += timePerMessageInMs; + results[size].sum_t2 += timePerMessageInMs * timePerMessageInMs; + } + config.addToLog(formatResultInKiB(size, timePerMessageInMs, -1, speed, + config.printSize)); +} + +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 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; +} + +var tasks = []; + +function runNextTask(config) { + var task = tasks.shift(); + if (task == undefined) { + config.addToLog('Finished'); + cleanup(); + 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) { + var multiplierIndex = 0; + for (var size = config.startSize; + size <= config.stopThreshold; + ++multiplierIndex) { + var task = stepFunc.bind( + null, + size, + config, + i < config.numWarmUpIterations); + tasks.push(task); + var multiplier = config.multipliers[ + multiplierIndex % config.multipliers.length]; + if (multiplier <= 1) { + config.addToLog('Invalid multiplier ' + multiplier); + config.notifyAbort(); + throw new Error('Invalid multipler'); + } + size = Math.ceil(size * multiplier); + } + } +} + +function addResultReportingTask(config, title) { + tasks.push(function(){ + timerID = null; + config.addToSummary(title); + reportAverageData(config); + 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 stop(config) { + clearTimeout(timerID); + timerID = null; + tasks = []; + config.addToLog('Stopped'); + cleanup(); +} + +var worker; + +function initWorker(origin) { + worker = new Worker(origin + '/benchmark.js'); +} + +function doAction(config, isWindowToWorker, action) { + if (isWindowToWorker) { + worker.onmessage = function(addToLog, addToSummary, + measureValue, notifyAbort, 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); + else if (message.data.type === 'notifyAbort') + notifyAbort(); + }.bind(undefined, config.addToLog, config.addToSummary, + config.measureValue, config.notifyAbort); + config.addToLog = undefined; + config.addToSummary = undefined; + config.measureValue = undefined; + config.notifyAbort = undefined; + worker.postMessage({type: action, config: config}); + } else { + if (action === 'sendBenchmark') + sendBenchmark(config); + else if (action === 'receiveBenchmark') + receiveBenchmark(config); + else if (action === 'batchBenchmark') + batchBenchmark(config); + else if (action === 'stop') + stop(config); + } +} diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_main.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_main.js new file mode 100644 index 0000000000..78add48731 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_main.js @@ -0,0 +1,89 @@ +// Copyright 2014, 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 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) { +} + +// config.notifyAbort is called when the benchmark failed and aborted, and +// intended to be used in Performance Tests. +// Do nothing here in non-PerformanceTest. +function notifyAbort() { +} + +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 getFloatArrayFromInput(id) { + var strArray = document.getElementById(id).value.split(','); + return strArray.map(parseFloat); +} diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_worker.js b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_worker.js new file mode 100644 index 0000000000..dd90449a90 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/example/util_worker.js @@ -0,0 +1,44 @@ +// Copyright 2014, 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 the worker threads only). + +onmessage = function (message) { + var config = message.data.config; + config.addToLog = function(text) { + postMessage({type: 'addToLog', data: text}); }; + config.addToSummary = function(text) { + postMessage({type: 'addToSummary', data: text}); }; + config.measureValue = function(value) { + postMessage({type: 'measureValue', data: value}); }; + config.notifyAbort = function() { postMessage({type: 'notifyAbort'}); }; + + doAction(config, false, message.data.type); +}; diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/__init__.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/__init__.py new file mode 100644 index 0000000000..28d5f5950f --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/__init__.py @@ -0,0 +1,172 @@ +# 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 Standalone WebSocket Server for testing purposes + +mod_pywebsocket is an API that provides WebSocket functionalities with +a standalone WebSocket server. It is intended for testing or +experimental purposes. + +Installation +============ +1. Follow standalone server documentation to start running the +standalone server. It can be read by running the following command: + + $ pydoc mod_pywebsocket.standalone + +2. Once the standalone server is launched verify it by accessing +http://localhost[:port]/console.html. Include the port number when +specified on launch. If everything is working correctly, you +will see a simple echo console. + + +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_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_extensions +- ws_deflate +- ws_protocol +- ws_requested_protocols + +The last two are a bit tricky. See the next subsection. + + +Subprotocol Negotiation +----------------------- + +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. + +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. The standalone +server uses threads by default. + + +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/third_party/pywebsocket3/mod_pywebsocket/_stream_exceptions.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/_stream_exceptions.py new file mode 100644 index 0000000000..b47878bc4a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/_stream_exceptions.py @@ -0,0 +1,82 @@ +# Copyright 2020, 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. +"""Stream Exceptions. +""" + +# 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. + + +# 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 + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/common.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/common.py new file mode 100644 index 0000000000..9cb11f15cb --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/common.py @@ -0,0 +1,273 @@ +# 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 __future__ import absolute_import +from mod_pywebsocket import http_header_util + +# Additional log level definitions. +LOGLEVEL_FINE = 9 + +# Constants indicating WebSocket protocol version. +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 + +# UUID for the opening handshake and frame masking. +WEBSOCKET_ACCEPT_UUID = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + +# Opening handshake header names and expected values. +UPGRADE_HEADER = 'Upgrade' +WEBSOCKET_UPGRADE_TYPE = 'websocket' +CONNECTION_HEADER = 'Connection' +UPGRADE_CONNECTION_TYPE = 'Upgrade' +HOST_HEADER = 'Host' +ORIGIN_HEADER = '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' + +# Extensions +PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate' + +# 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): + """This 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 the extension name.""" + return self._name + + def add_parameter(self, name, value): + """Add a parameter.""" + self._parameters.append((name, value)) + + def get_parameters(self): + """Return the parameters.""" + return self._parameters + + def get_parameter_names(self): + """Return the names of the parameters.""" + return [name for name, unused_value in self._parameters] + + def has_parameter(self, name): + """Test if a parameter exists.""" + for param_name, param_value in self._parameters: + if param_name == name: + return True + return False + + def get_parameter_value(self, name): + """Get the value of a specific parameter.""" + for param_name, param_value in self._parameters: + if param_name == name: + return param_value + + +class ExtensionParsingException(Exception): + """Exception to handle errors in extension parsing.""" + 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 as e: + raise ExtensionParsingException( + 'Failed to parse parameter for %r (%r)' % (extension_token, e)) + + return extension + + +def parse_extensions(data): + """Parse 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): + """Format 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): + """Format 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/third_party/pywebsocket3/mod_pywebsocket/dispatch.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/dispatch.py new file mode 100644 index 0000000000..4ee943a5b8 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/dispatch.py @@ -0,0 +1,385 @@ +# 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. +""" + +from __future__ import absolute_import +import logging +import os +import re +import traceback + +from mod_pywebsocket import common +from mod_pywebsocket import handshake +from mod_pywebsocket import msgutil +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: + # This statement is gramatically different in python 2 and 3. + # Hence, yapf will complain about this. To overcome this, we disable + # yapf for this line. + exec(handler_definition, global_dic) # yapf: disable + except Exception: + raise DispatchException('Error in sourcing handler:' + + traceback.format_exc()) + 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 as e: + # Re-raise to tell the caller of this function to finish this + # connection without sending any error. + self._logger.debug('%s', traceback.format_exc()) + raise + except Exception as 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: + 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 as e: + self._logger.debug('%s', traceback.format_exc()) + raise + except msgutil.BadOperationException as e: + self._logger.debug('%s', e) + request.ws_stream.close_connection( + common.STATUS_INTERNAL_ENDPOINT_ERROR) + except msgutil.InvalidFrameException as 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 as e: + self._logger.debug('%s', e) + request.ws_stream.close_connection(common.STATUS_UNSUPPORTED_DATA) + except stream.InvalidUTF8Exception as e: + self._logger.debug('%s', e) + request.ws_stream.close_connection( + common.STATUS_INVALID_FRAME_PAYLOAD_DATA) + except msgutil.ConnectionTerminatedException as e: + self._logger.debug('%s', e) + except Exception as 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: + with open(path) as handler_file: + handler_suite = _source_handler_file(handler_file.read()) + except DispatchException as 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/third_party/pywebsocket3/mod_pywebsocket/extensions.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/extensions.py new file mode 100644 index 0000000000..314a949d45 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/extensions.py @@ -0,0 +1,474 @@ +# 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 __future__ import absolute_import +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 = {} + + +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 PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface): + """permessage-deflate extension processor. + + 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): + """Construct PerMessageDeflateExtensionProcessor.""" + + ExtensionProcessorInterface.__init__(self, request) + self._logger = util.get_class_logger(self) + + self._preferred_client_max_window_bits = None + self._client_no_context_takeover = False + + def name(self): + # This method returns "deflate" (not "permessage-deflate") for + # compatibility. + return 'deflate' + + def _get_extension_response_internal(self): + 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 + + 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 as 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 (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 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) + + +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) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/fast_masking.i b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/fast_masking.i new file mode 100644 index 0000000000..ddaad27f53 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/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/third_party/pywebsocket3/mod_pywebsocket/handshake/__init__.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/__init__.py new file mode 100644 index 0000000000..4bc1c67c57 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/__init__.py @@ -0,0 +1,101 @@ +# 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. +""" + +from __future__ import absolute_import +import logging + +from mod_pywebsocket import common +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): + """Performs WebSocket handshake. + + Args: + request: mod_python request. + dispatcher: Dispatcher (dispatch.Dispatcher). + + 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))) + + 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 as e: + _LOGGER.debug( + 'Failed to complete opening handshake as %s protocol: %r', + name, e) + if e.status: + raise e + except AbortedByUserException as e: + raise + except VersionException as 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/third_party/pywebsocket3/mod_pywebsocket/handshake/base.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/base.py new file mode 100644 index 0000000000..ffad0614d6 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/base.py @@ -0,0 +1,396 @@ +# 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 __future__ import absolute_import + +from mod_pywebsocket import common +from mod_pywebsocket import http_header_util +from mod_pywebsocket.extensions import get_extension_processor +from mod_pywebsocket.stream import StreamOptions +from mod_pywebsocket.stream import Stream +from mod_pywebsocket import util + +from six.moves import map +from six.moves import range + +# Defining aliases for values used frequently. +_VERSION_LATEST = common.VERSION_HYBI_LATEST +_VERSION_LATEST_STRING = str(_VERSION_LATEST) +_SUPPORTED_VERSIONS = [ + _VERSION_LATEST, +] + + +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. '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 as e: + raise HandshakeException('Invalid port number format: %r' % e) + + +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 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 + + +class HandshakerBase(object): + def __init__(self, request, dispatcher): + self._logger = util.get_class_logger(self) + self._request = request + self._dispatcher = dispatcher + + """ subclasses must implement the five following methods """ + + def _protocol_rfc(self): + """ Return the name of the RFC that the handshake class is implementing. + """ + + raise AssertionError("subclasses should implement this method") + + def _transform_header(self, header): + """ + :param header: header name + + transform the header name if needed. For example, HTTP/2 subclass will + return the name of the header in lower case. + """ + + raise AssertionError("subclasses should implement this method") + + def _validate_request(self): + """ validate that all the mandatory fields are set """ + + raise AssertionError("subclasses should implement this method") + + def _set_accept(self): + """ Computes accept value based on Sec-WebSocket-Accept if needed. """ + + raise AssertionError("subclasses should implement this method") + + def _send_handshake(self): + """ Prepare and send the response after it has been parsed and processed. + """ + + raise AssertionError("subclasses should implement this method") + + def do_handshake(self): + self._request.ws_close_code = None + self._request.ws_close_reason = None + + # Parsing. + self._validate_request() + self._request.ws_resource = self._request.uri + self._request.ws_version = self._check_version() + + try: + self._get_origin() + self._set_protocol() + self._parse_extensions() + + self._set_accept() + + self._logger.debug('Protocol version is ' + self._protocol_rfc()) + + # Setup extension processors. + self._request.ws_extension_processors = self._get_extension_processors_requested( + ) + + # 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) + + stream_options = StreamOptions() + self._process_extensions(stream_options) + + self._request.ws_stream = Stream(self._request, 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() + except HandshakeException as e: + if not e.status: + # Fallback to 400 bad request by default. + e.status = common.HTTP_STATUS_BAD_REQUEST + raise e + + def _check_version(self): + sec_websocket_version_header = self._transform_header( + common.SEC_WEBSOCKET_VERSION_HEADER) + version = get_mandatory_header(self._request, + 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, sec_websocket_version_header), + status=common.HTTP_STATUS_BAD_REQUEST) + raise VersionException('Unsupported version %r for header %s' % + (version, sec_websocket_version_header), + supported_versions=', '.join( + map(str, _SUPPORTED_VERSIONS))) + + def _get_origin(self): + origin_header = self._transform_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 _set_protocol(self): + self._request.ws_protocol = None + + sec_websocket_protocol_header = self._transform_header( + common.SEC_WEBSOCKET_PROTOCOL_HEADER) + protocol_header = self._request.headers_in.get( + 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): + sec_websocket_extensions_header = self._transform_header( + common.SEC_WEBSOCKET_EXTENSIONS_HEADER) + extensions_header = self._request.headers_in.get( + 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 as e: + raise HandshakeException( + 'Failed to parse sec-websocket-extensions header: %r' % e) + + self._logger.debug( + 'Extensions requested: %r', + list( + map(common.ExtensionParameter.name, + self._request.ws_requested_extensions))) + + def _get_extension_processors_requested(self): + 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) + return processors + + def _process_extensions(self, stream_options): + processors = [ + processor for processor in self._request.ws_extension_processors + if processor is not None + ] + + # 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 = [ + processor for processor in processors if processor.is_active() + ] + + accepted_extensions = [] + + 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) + + # Inactivate all of the following compression extensions. + for j in range(index + 1, len(processors)): + processors[j].set_active(False) + + if len(accepted_extensions) > 0: + self._request.ws_extensions = accepted_extensions + self._logger.debug( + 'Extensions accepted: %r', + list(map(common.ExtensionParameter.name, accepted_extensions))) + else: + self._request.ws_extensions = None + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/hybi.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/hybi.py new file mode 100644 index 0000000000..cf931db5a5 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/handshake/hybi.py @@ -0,0 +1,223 @@ +# 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 +""" + +from __future__ import absolute_import +import base64 +import re +from hashlib import sha1 + +from mod_pywebsocket import common +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 HandshakerBase +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]==$') + + +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 != u'GET': + raise HandshakeException('Method is not GET: %r' % request.method) + + if request.protocol != u'HTTP/1.1': + raise HandshakeException('Version is not HTTP/1.1: %r' % + request.protocol) + + +def compute_accept(key): + """Computes value for the Sec-WebSocket-Accept header from value of the + Sec-WebSocket-Key header. + """ + + accept_binary = sha1(key + common.WEBSOCKET_ACCEPT_UUID).digest() + accept = base64.b64encode(accept_binary) + + return accept + + +def compute_accept_from_unicode(unicode_key): + """A wrapper function for compute_accept which takes a unicode string as an + argument, and encodes it to byte string. It then passes it on to + compute_accept. + """ + + key = unicode_key.encode('UTF-8') + return compute_accept(key) + + +def format_header(name, value): + return u'%s: %s\r\n' % (name, value) + + +class Handshaker(HandshakerBase): + """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. + """ + super(Handshaker, self).__init__(request, dispatcher) + + def _transform_header(self, header): + return header + + def _protocol_rfc(self): + return 'RFC 6455' + + def _validate_connection_header(self): + connection = get_mandatory_header(self._request, + common.CONNECTION_HEADER) + + try: + connection_tokens = parse_token_list(connection) + except HandshakeException as 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 _validate_request(self): + check_request_line(self._request) + validate_mandatory_header(self._request, common.UPGRADE_HEADER, + common.WEBSOCKET_UPGRADE_TYPE) + self._validate_connection_header() + unused_host = get_mandatory_header(self._request, common.HOST_HEADER) + + def _set_accept(self): + # Key validation, response generation. + key = self._get_key() + accept = compute_accept(key) + self._logger.debug('%s: %r (%s)', common.SEC_WEBSOCKET_ACCEPT_HEADER, + accept, util.hexify(base64.b64decode(accept))) + self._request._accept = accept + + 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 as 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.encode('UTF-8') + + def _create_handshake_response(self, accept): + response = [] + + response.append(u'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.decode('UTF-8'))) + 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(u'\r\n') + + return u''.join(response) + + def _send_handshake(self): + raw_response = self._create_handshake_response(self._request._accept) + self._request.connection.write(raw_response.encode('UTF-8')) + 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/third_party/pywebsocket3/mod_pywebsocket/http_header_util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/http_header_util.py new file mode 100644 index 0000000000..21fde59af1 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/http_header_util.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. +"""Utilities for parsing and formatting headers that follow the grammar defined +in HTTP RFC http://www.ietf.org/rfc/rfc2616.txt. +""" + +from __future__ import absolute_import +import six.moves.urllib.parse + +_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): + r"""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 = six.moves.urllib.parse.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: + # The port property cause ValueError on invalid null port descriptions + # like 'ws://host:INVALID_PORT/path', where the assigned port is not + # *DIGIT. For python 3.6 and later, ValueError also raises when + # assigning invalid port numbers such as 'ws://host:-1/path'. Earlier + # versions simply return None and ignore invalid port attributes. + 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 + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/memorizingfile.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/memorizingfile.py new file mode 100644 index 0000000000..d353967618 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/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. +""" + +from __future__ import absolute_import +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.maxsize): + """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): + """Return a file attribute. + + Returns the value overridden by this class for some attributes, + and forwards the call to _file for the other attributes. + """ + 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/third_party/pywebsocket3/mod_pywebsocket/msgutil.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/msgutil.py new file mode 100644 index 0000000000..f58ca78e14 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/msgutil.py @@ -0,0 +1,214 @@ +# 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. +""" + +from __future__ import absolute_import +import six.moves.queue +import threading + +# Export Exception symbols from msgutil for backward compatibility +from mod_pywebsocket._stream_exceptions import ConnectionTerminatedException +from mod_pywebsocket._stream_exceptions import InvalidFrameException +from mod_pywebsocket._stream_exceptions import BadOperationException +from mod_pywebsocket._stream_exceptions 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 = six.moves.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 six.moves.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 = six.moves.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/third_party/pywebsocket3/mod_pywebsocket/request_handler.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/request_handler.py new file mode 100644 index 0000000000..5e9c875dc7 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/request_handler.py @@ -0,0 +1,319 @@ +# Copyright 2020, 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. +"""Request Handler and Request/Connection classes for standalone server. +""" + +import os + +from six.moves import CGIHTTPServer +from six.moves import http_client + +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 + +# 1024 is practically large enough to contain WebSocket handshake lines. +_MAX_MEMORIZED_LINES = 1024 + + +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 + + +class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): + """CGIHTTPRequestHandler specialized for WebSocket.""" + + # Use httplib.HTTPMessage instead of mimetools.Message. + MessageClass = http_client.HTTPMessage + + 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._options.use_basic_auth: + auth = self.headers.get('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) + 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 as 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) + except handshake.VersionException as 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 as 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 as 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 + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/server_util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/server_util.py new file mode 100644 index 0000000000..8f9e273e97 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/server_util.py @@ -0,0 +1,87 @@ +# Copyright 2020, 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. +"""Server related utilities.""" + +import logging +import logging.handlers +import threading +import time + +from mod_pywebsocket import common +from mod_pywebsocket import util + + +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) + + +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) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/standalone.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/standalone.py new file mode 100755 index 0000000000..0a3bcdbacd --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/standalone.py @@ -0,0 +1,481 @@ +#!/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 as a standalone 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. +""" + +from __future__ import absolute_import +from six.moves import configparser +import base64 +import logging +import argparse +import os +import six +import sys +import traceback + +from mod_pywebsocket import common +from mod_pywebsocket import util +from mod_pywebsocket import server_util +from mod_pywebsocket.websocket_server import WebSocketServer + +_DEFAULT_LOG_MAX_BYTES = 1024 * 256 +_DEFAULT_LOG_BACKUP_COUNT = 5 + +_DEFAULT_REQUEST_QUEUE_SIZE = 128 + + +def _build_option_parser(): + parser = argparse.ArgumentParser() + + parser.add_argument( + '--config', + dest='config_file', + type=six.text_type, + default=None, + help=('Path to configuration file. See the file comment ' + 'at the top of this file for the configuration ' + 'file format')) + parser.add_argument('-H', + '--server-host', + '--server_host', + dest='server_host', + default='', + help='server hostname to listen to') + parser.add_argument('-V', + '--validation-host', + '--validation_host', + dest='validation_host', + default=None, + help='server hostname to validate in absolute path.') + parser.add_argument('-p', + '--port', + dest='port', + type=int, + default=common.DEFAULT_WEB_SOCKET_PORT, + help='port to listen to') + parser.add_argument('-P', + '--validation-port', + '--validation_port', + dest='validation_port', + type=int, + default=None, + help='server port to validate in absolute path.') + parser.add_argument( + '-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_argument('-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_argument('-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_argument( + '--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_argument('-d', + '--document-root', + '--document_root', + dest='document_root', + default='.', + help='Document root directory.') + parser.add_argument('-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_argument('-t', + '--tls', + dest='use_tls', + action='store_true', + default=False, + help='use TLS (wss://)') + parser.add_argument('-k', + '--private-key', + '--private_key', + dest='private_key', + default='', + help='TLS private key file.') + parser.add_argument('-c', + '--certificate', + dest='certificate', + default='', + help='TLS certificate file.') + parser.add_argument('--tls-client-auth', + dest='tls_client_auth', + action='store_true', + default=False, + help='Requests TLS client auth on every connection.') + parser.add_argument('--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_argument('--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_argument('--basic-auth', + dest='use_basic_auth', + action='store_true', + default=False, + help='Requires Basic authentication.') + parser.add_argument( + '--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_argument('-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_argument('--log-level', + '--log_level', + type=six.text_type, + dest='log_level', + default='warn', + choices=[ + 'fine', 'debug', 'info', 'warning', 'warn', + 'error', 'critical' + ], + help='Log level.') + parser.add_argument( + '--deflate-log-level', + '--deflate_log_level', + type=six.text_type, + dest='deflate_log_level', + default='warn', + choices=['debug', 'info', 'warning', 'warn', 'error', 'critical'], + help='Log level for _Deflater and _Inflater.') + parser.add_argument('--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_argument('--log-max', + '--log_max', + dest='log_max', + type=int, + default=_DEFAULT_LOG_MAX_BYTES, + help='Log maximum bytes') + parser.add_argument('--log-count', + '--log_count', + dest='log_count', + type=int, + default=_DEFAULT_LOG_BACKUP_COUNT, + help='Log backup count') + parser.add_argument('-q', + '--queue', + dest='request_queue_size', + type=int, + default=_DEFAULT_REQUEST_QUEUE_SIZE, + help='request queue size') + + return parser + + +def _parse_args_and_config(args): + parser = _build_option_parser() + + # First, parse options without configuration file. + temporary_options, temporary_args = parser.parse_known_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 as 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_known_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, it changes the current directory. + """ + + options, args = _parse_args_and_config(args=args) + + os.chdir(options.document_root) + + server_util.configure_logging(options) + + # 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'] + + def __check_script(scriptpath): + return util.get_script_interp(scriptpath, cygwin_path) + + options.is_executable_method = __check_script + + if options.use_tls: + logging.debug('Using ssl module') + + 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_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.encode('UTF-8')).decode() + + try: + if options.thread_monitor_interval_in_sec > 0: + # Run a thread monitor to show the status of server threads for + # debugging. + server_util.ThreadMonitor( + options.thread_monitor_interval_in_sec).start() + + server = WebSocketServer(options) + server.serve_forever() + except Exception as e: + logging.critical('mod_pywebsocket: %s' % e) + logging.critical('mod_pywebsocket: %s' % traceback.format_exc()) + sys.exit(1) + + +if __name__ == '__main__': + _main(sys.argv[1:]) + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/stream.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/stream.py new file mode 100644 index 0000000000..82d1ea619c --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/stream.py @@ -0,0 +1,950 @@ +# 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 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 +import socket +import six + +from mod_pywebsocket import common +from mod_pywebsocket import util +from mod_pywebsocket._stream_exceptions import BadOperationException +from mod_pywebsocket._stream_exceptions import ConnectionTerminatedException +from mod_pywebsocket._stream_exceptions import InvalidFrameException +from mod_pywebsocket._stream_exceptions import InvalidUTF8Exception +from mod_pywebsocket._stream_exceptions import UnsupportedFrameException + +_NOOP_MASKER = util.NoopMasker() + + +class Frame(object): + def __init__(self, + fin=1, + rsv1=0, + rsv2=0, + rsv3=0, + opcode=None, + payload=b''): + 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 util.pack_byte(mask_bit | length) + elif length < (1 << 16): + return util.pack_byte(mask_bit | 126) + struct.pack('!H', length) + elif length < (1 << 63): + return util.pack_byte(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 = b'' + + first_byte = ((fin << 7) + | (rsv1 << 6) | (rsv2 << 5) | (rsv3 << 4) + | opcode) + header += util.pack_byte(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') + + first_byte = ord(receive_bytes(1)) + 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(receive_bytes(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 = b'' + 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(object): + """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. + """ + + self._logger = util.get_class_logger(self) + + self._options = options + self._request = request + + 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 _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 IOError as 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 as 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. This method returns byte strings. + + 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 b''.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 b''.join(read_bytes) + + 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, six.text_type): + raise BadOperationException( + 'Message for binary frame must not be instance of Unicode') + + 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 as 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 = b''.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: + 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: + # 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: + 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. + + 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 as 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. Note that this method blocks until + it receives acknowledgement to the closing handshake. + + 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 + + # When we receive a close frame, we call _process_close_message(). + # _process_close_message() immediately acknowledges to the + # server-initiated closing handshake and sets server_terminated to + # True. So, here we can assume that we haven't received any close + # frame. We're initiating a closing handshake. + + 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, bytes) and not isinstance( + reason, six.text_type): + raise BadOperationException( + 'close reason must be an instance of bytes 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, binary=False): + if not binary and isinstance(body, six.text_type): + body = body.encode('UTF-8') + 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/third_party/pywebsocket3/mod_pywebsocket/util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/util.py new file mode 100644 index 0000000000..04006ecacd --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/util.py @@ -0,0 +1,386 @@ +# 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.""" + +from __future__ import absolute_import +import array +import errno +import logging +import os +import re +import six +from six.moves import map +from six.moves import range +import socket +import struct +import zlib + +try: + from mod_pywebsocket import fast_masking +except ImportError: + pass + + +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): + r"""Get #!-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 hexify(s): + return ' '.join(['%02x' % x for x in six.iterbytes(s)]) + + +def get_class_logger(o): + """Return the logging class information.""" + return logging.getLogger('%s.%s' % + (o.__class__.__module__, o.__class__.__name__)) + + +def pack_byte(b): + """Pack an integer to network-ordered byte""" + return struct.pack('!B', b) + + +class NoopMasker(object): + """A NoOp masking object. + + This has the same interface as RepeatedXorMasker but just returns + the string passed in without making any change. + """ + def __init__(self): + """NoOp.""" + pass + + def mask(self, s): + """NoOp.""" + return s + + +class RepeatedXorMasker(object): + """A masking object that applies XOR on the string. + + Applies XOR on the byte 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): + """Perform the mask via SWIG.""" + 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): + """Perform the mask via python.""" + if isinstance(s, six.text_type): + raise Exception( + 'Masking Operation should not process unicode strings') + + result = bytearray(s) + + # Use temporary local variables to eliminate the cost to access + # attributes + masking_key = [c for c in six.iterbytes(self._masking_key)] + masking_key_size = len(masking_key) + masking_key_index = self._masking_key_index + + for i in range(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 bytes(result) + + 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) + + # Using the smallest window bits of 9 for generating input frames. + # On WebSocket spec, the smallest window bit is 8. However, zlib does + # not accept window_bit = 8. + # + # Because of a zlib deflate quirk, back-references will not use the + # entire range of 1 << window_bits, but will instead use a restricted + # range of (1 << window_bits) - 262. With an increased window_bits = 9, + # back-references will be within a range of 250. These can still be + # decompressed with window_bits = 8 and the 256-byte window used there. + # + # Similar disscussions can be found in https://crbug.com/691074 + window_bits = max(window_bits, 9) + + 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 = b'' + + self.reset() + + def decompress(self, size): + if not (size == -1 or size > 0): + raise Exception('size must be -1 or positive') + + data = b'' + + while True: + data += self._decompress.decompress(self._unconsumed, + max(0, 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 + pack_byte(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 a la RFC1979. + + 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 + b'\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 b'' + 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/third_party/pywebsocket3/mod_pywebsocket/websocket_server.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/websocket_server.py new file mode 100644 index 0000000000..fa24bb9651 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/mod_pywebsocket/websocket_server.py @@ -0,0 +1,285 @@ +# Copyright 2020, 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 WebsocketServer + +This file deals with the main module of standalone server. Although it is fine +to import this file directly to use WebSocketServer, it is strongly recommended +to use standalone.py, since it is intended to act as a skeleton of this module. +""" + +from __future__ import absolute_import +from six.moves import BaseHTTPServer +from six.moves import socketserver +import logging +import re +import select +import socket +import ssl +import threading +import traceback + +from mod_pywebsocket import dispatch +from mod_pywebsocket import util +from mod_pywebsocket.request_handler import WebSocketRequestHandler + + +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 + """ + + with open(websock_handlers_map_file) as f: + for line in f: + if line[0] == '#' or line.isspace(): + continue + m = re.match(r'(\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 as e: + logging.error(str(e)) + + +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 as e: + self._logger.info('Skip by failure: %r', e) + continue + server_options = self.websocket_server_options + if server_options.use_tls: + 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, + ca_certs=server_options.tls_client_ca, + cert_reqs=client_cert_) + 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 as 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 as 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, traceback.format_exc()) + # Note: client_address is a tuple. + + def get_request(self): + """Override TCPServer.get_request.""" + + accepted_socket, client_address = self.socket.accept() + + server_options = self.websocket_server_options + if server_options.use_tls: + # 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()) + + 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() + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.py new file mode 100755 index 0000000000..b65904c94f --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/setup.py @@ -0,0 +1,73 @@ +#!/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 __future__ import absolute_import +from __future__ import print_function +from setuptools 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 + +# This is used since python_requires field is not recognized with +# pip version 9.0.0 and earlier +if sys.hexversion < 0x020700f0: + print('%s requires Python 2.7 or later.' % _PACKAGE_NAME, file=sys.stderr) + 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='Standalone WebSocket Server for testing purposes.', + long_description=('mod_pywebsocket is a standalone server for ' + 'the WebSocket Protocol (RFC 6455). ' + 'See mod_pywebsocket/__init__.py for more detail.'), + license='See LICENSE', + name=_PACKAGE_NAME, + packages=[_PACKAGE_NAME, _PACKAGE_NAME + '.handshake'], + python_requires='>=2.7', + install_requires=['six'], + url='https://github.com/GoogleChromeLabs/pywebsocket3', + version='3.0.1', +) + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/__init__.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/__init__.py diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cacert.pem b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cacert.pem new file mode 100644 index 0000000000..4dadae121b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/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/third_party/pywebsocket3/test/cert/cert.pem b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/cert.pem new file mode 100644 index 0000000000..25379a72b0 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/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/third_party/pywebsocket3/test/cert/client_cert.p12 b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/client_cert.p12 Binary files differnew file mode 100644 index 0000000000..14e1399279 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/client_cert.p12 diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/key.pem b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/cert/key.pem new file mode 100644 index 0000000000..fae858318f --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/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/third_party/pywebsocket3/test/client_for_testing.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/client_for_testing.py new file mode 100644 index 0000000000..a45e8f5cf2 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/client_for_testing.py @@ -0,0 +1,726 @@ +#!/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. +""" + +from __future__ import absolute_import +import base64 +import errno +import logging +import os +import random +import re +import socket +import struct +import time +from hashlib import sha1 +from six import iterbytes +from six import indexbytes + +from mod_pywebsocket import common +from mod_pywebsocket import util +from mod_pywebsocket.handshake import HandshakeException + +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' +_CONNECTION_HEADER = 'Connection: Upgrade\r\n' + +WEBSOCKET_ACCEPT_UUID = b'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 +_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 b''.join(received_bytes) + + +# TODO(tyoshino): Now the WebSocketHandshake class diverts these methods. We +# should move to HTTP parser as specified in RFC 6455. + + +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 = b'' + value = b'' + # 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 != b'\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.decode('UTF-8'), + []).append(value.decode('UTF-8')) + # 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 = b'' + while True: + # 4.1 34. read a byte from the server + ch = receive_bytes(socket, 1) + if ch == b'\r': # 0x0D + return None + elif ch == b'\n': # 0x0A + raise Exception('Unexpected LF when reading header name %r' % name) + elif ch == b':': # 0x3A + return name.lower() + 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 == b' ': # 0x20 + continue + return ch + + +def _read_value(socket, ch): + # 4.1 33. let /value/ be empty byte arrays + value = b'' + # 4.1 36. read a byte from server. + while True: + if ch == b'\r': # 0x0D + return value + elif ch == b'\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): + + first_byte = ord(receive_bytes(socket, 1)) + 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(receive_bytes(socket, 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.encode('UTF-8')) + + 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 == 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(u'Sec-WebSocket-Key: %s\r\n' % key.decode('UTF-8')) + + fields.append(u'Sec-WebSocket-Version: %d\r\n' % self._options.version) + + if self._options.use_basic_auth: + credential = 'Basic ' + base64.b64encode( + self._options.basic_auth_credential.encode('UTF-8')).decode() + fields.append(u'Authorization: %s\r\n' % credential) + + # Setting up extensions. + if len(self._options.extensions) > 0: + fields.append(u'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.encode('UTF-8')) + self._socket.sendall(b'\r\n') + + self._logger.info('Sent opening handshake request') + + field = b'' + while True: + ch = receive_bytes(self._socket, 1) + field += ch + if ch == b'\n': + break + + self._logger.debug('Opening handshake Response-Line: %r', field) + + # Will raise a UnicodeDecodeError when the decode fails + if len(field) < 7 or not field.endswith(b'\r\n'): + raise Exception('Wrong status line: %s' % field.decode('Latin-1')) + m = re.match(b'[^ ]* ([^ ]*) .*', field) + if m is None: + raise Exception('No HTTP status code found in status line: %s' % + field.decode('Latin-1')) + code = m.group(1) + if not re.match(b'[0-9][0-9][0-9]$', code): + raise Exception( + 'HTTP status code %s is not three digit in status line: %s' % + (code.decode('Latin-1'), field.decode('Latin-1'))) + if code != b'101': + raise HttpStatusException( + 'Expected HTTP status code 101 but found %s in status line: ' + '%r' % (code.decode('Latin-1'), field.decode('Latin-1')), + int(code)) + fields = _read_fields(self._socket) + ch = receive_bytes(self._socket, 1) + if ch != b'\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 as 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 = sha1(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.decode('UTF-8'): + 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. + for extension in accepted_extensions: + 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()) + + +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 + + 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 iterbytes(s): + result.append(util.pack_byte(c ^ indexbytes(masking_nonce, count))) + count = (count + 1) % len(masking_nonce) + return b''.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 mask: + mask_bit = 1 << 7 + else: + mask_bit = 0 + + header = util.pack_byte(fin << 7 | rsv1 << 6 | rsv2 << 5 | rsv3 << 4 + | opcode) + payload_length = len(payload) + if payload_length <= 125: + header += util.pack_byte(mask_bit | payload_length) + elif payload_length < 1 << 16: + header += util.pack_byte(mask_bit | 126) + struct.pack( + '!H', payload_length) + elif payload_length < 1 << 63: + header += util.pack_byte(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 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 = util.pack_byte(1 << 7 | OPCODE_CLOSE) + + if code is not None: + body = struct.pack('!H', code) + reason.encode('utf-8') + else: + body = b'' + if mask: + frame += util.pack_byte(1 << 7 | len(body)) + self._mask_hybi(body) + else: + frame += util.pack_byte(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 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.use_basic_auth = False + self.basic_auth_credential = 'test:test' + self.extensions = [] + + +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 as 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 as 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) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/mock.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/mock.py new file mode 100644 index 0000000000..eeaef52ecf --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/mock.py @@ -0,0 +1,227 @@ +# 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. +""" + +from __future__ import absolute_import +import six.moves.queue +import threading +import struct +import six + +from mod_pywebsocket import common +from mod_pywebsocket import util +from mod_pywebsocket.stream import Stream +from mod_pywebsocket.stream import StreamOptions +from six.moves import range + + +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 = b'fake_address' + + def write(self, data): + """Override mod_python.apache.mp_conn.write. + + data should be bytes when touching this method manually. + """ + + self._write_data.append(data) + + def written_data(self): + """Get bytes written to this mock.""" + + return b''.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 b'' + end_index = self._read_data.find(b'\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 b'' + 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 = six.moves.queue.Queue() + + def readline(self): + """Override mod_python.apache.mp_conn.readline.""" + line = bytearray() + while True: + c = self._queue.get() + line.append(c) + if c == ord(b'\n'): + return bytes(line) + + def read(self, length): + """Override mod_python.apache.mp_conn.read.""" + + data = bytearray() + for unused in range(length): + data.append(self._queue.get()) + + return bytes(data) + + def put_bytes(self, bytes): + """Put bytes to be read from this mock. + + Args: + bytes: bytes to be read. + """ + + for byte in six.iterbytes(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 = list(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 = Stream(self, StreamOptions()) + self.ws_close_code = None + self.ws_close_reason = None + self.ws_version = common.VERSION_HYBI_LATEST + 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/third_party/pywebsocket3/test/run_all.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/run_all.py new file mode 100755 index 0000000000..ea52223cea --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/run_all.py @@ -0,0 +1,88 @@ +#!/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 +""" + +from __future__ import absolute_import +import logging +import argparse +import os +import re +import six +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.dirname(__file__))) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument( + '--log-level', + '--log_level', + type=six.text_type, + dest='log_level', + default='warning', + choices=['debug', 'info', 'warning', 'warn', 'error', 'critical']) + options, args = parser.parse_known_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/third_party/pywebsocket3/test/set_sys_path.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/set_sys_path.py new file mode 100644 index 0000000000..48d0e116a5 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/set_sys_path.py @@ -0,0 +1,41 @@ +# 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. +""" + +from __future__ import absolute_import +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.dirname(__file__), '..')) + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_dispatch.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_dispatch.py new file mode 100755 index 0000000000..132dd92d76 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_dispatch.py @@ -0,0 +1,298 @@ +#!/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.""" + +from __future__ import absolute_import +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 +from six.moves import zip + +_TEST_HANDLERS_DIR = os.path.join(os.path.dirname(__file__), '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.assertTrue( + 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.assertEqual(4, len(warnings)) + for expected, actual in zip(expected_warnings, warnings): + self.assertEqual(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 as e: + self.assertEqual(403, e.status) + except Exception as 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(b'\x88\x02\x03\xe8')) + request.ws_resource = '/origin_check' + request.ws_protocol = 'p1' + dispatcher.transfer_data(request) + self.assertEqual( + b'origin_check_wsh.py is called for /origin_check, p1' + b'\x88\x02\x03\xe8', request.connection.written_data()) + + request = mock.MockRequest( + connection=mock.MockConn(b'\x88\x02\x03\xe8')) + request.ws_resource = '/sub/plain' + request.ws_protocol = None + dispatcher.transfer_data(request) + self.assertEqual( + b'sub/plain_wsh.py is called for /sub/plain, None' + b'\x88\x02\x03\xe8', request.connection.written_data()) + + request = mock.MockRequest( + connection=mock.MockConn(b'\x88\x02\x03\xe8')) + request.ws_resource = '/sub/plain?' + request.ws_protocol = None + dispatcher.transfer_data(request) + self.assertEqual( + b'sub/plain_wsh.py is called for /sub/plain?, None' + b'\x88\x02\x03\xe8', request.connection.written_data()) + + request = mock.MockRequest( + connection=mock.MockConn(b'\x88\x02\x03\xe8')) + request.ws_resource = '/sub/plain?q=v' + request.ws_protocol = None + dispatcher.transfer_data(request) + self.assertEqual( + b'sub/plain_wsh.py is called for /sub/plain?q=v, None' + b'\x88\x02\x03\xe8', 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(b'')) + request.ws_resource = resource + request.ws_protocol = 'p2' + try: + dispatcher.transfer_data(request) + self.fail() + except dispatch.DispatchException as e: + self.assertTrue(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(b'')) + request.ws_resource = '/sub/exception_in_transfer' + request.ws_protocol = 'p3' + try: + dispatcher.transfer_data(request) + self.fail() + except Exception as e: + self.assertTrue( + 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.assertTrue('/origin_check' in disp._handler_suite_map) + self.assertTrue( + '/sub/exception_in_transfer' in disp._handler_suite_map) + self.assertTrue('/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.assertFalse('/origin_check' in disp._handler_suite_map) + self.assertTrue( + '/sub/exception_in_transfer' in disp._handler_suite_map) + self.assertTrue('/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.assertFalse('/origin_check' in disp._handler_suite_map) + self.assertFalse( + '/sub/exception_in_transfer' in disp._handler_suite_map) + self.assertFalse('/sub/plain' in disp._handler_suite_map) + self.assertTrue('/exception_in_transfer' in disp._handler_suite_map) + self.assertTrue('/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.assertTrue('/origin_check' in disp._handler_suite_map) + self.assertTrue( + '/sub/exception_in_transfer' in disp._handler_suite_map) + self.assertTrue('/sub/plain' in disp._handler_suite_map) + self.assertTrue('/' 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/third_party/pywebsocket3/test/test_endtoend.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_endtoend.py new file mode 100755 index 0000000000..2789e4a57e --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_endtoend.py @@ -0,0 +1,738 @@ +#!/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. +""" + +from __future__ import absolute_import +from six.moves import urllib +import locale +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 + +# Special message that tells the echo server to start closing handshake +_GOODBYE_MESSAGE = 'Goodbye' + +_SERVER_WARMUP_IN_SEC = 0.2 + + +# 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(b'binary', binary=True) + client.assert_receive(b'binary', binary=True) + client.send_message(b'\x00\x80\xfe\xff\x00\x80', binary=True) + client.assert_receive(b'\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 _check_handshake_with_basic_auth(client): + client.connect() + + client.send_message(_GOODBYE_MESSAGE) + client.assert_receive(_GOODBYE_MESSAGE) + + client.assert_receive_close() + client.send_close() + + client.assert_connection_closed() + + +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.dirname(__file__), '..') + 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' + + 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): + close_fds = True if sys.platform != 'win32' else None + return subprocess.Popen([sys.executable] + commandline, + close_fds=close_fds, + stdout=stdout, + stderr=stderr) + + def _run_server(self, extra_args=[]): + 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()) + + args += extra_args + + return self._run_python_command(args, stderr=self.server_stderr) + + def _close_server(self, server): + """ + + This method mimics Popen.__exit__ to gracefully kill the server process. + Its main purpose is to maintain comptaibility between python 2 and 3, + since Popen in python 2 does not have __exit__ attribute. + + """ + server.kill() + + if server.stdout: + server.stdout.close() + if server.stderr: + server.stderr.close() + if server.stdin: + server.stdin.close() + + server.wait() + + +class EndToEndHyBiTest(EndToEndTestBase): + def setUp(self): + EndToEndTestBase.setUp(self) + + def _run_test_with_options(self, + test_function, + options, + server_options=[]): + server = self._run_server(server_options) + 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._close_server(server) + + def _run_test(self, test_function): + self._run_test_with_options(test_function, self._options) + + 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._close_server(server) + + def _run_close_with_code_and_reason_test(self, + test_function, + code, + reason, + server_options=[]): + 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._close_server(server) + + 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 as e: + self.assertEqual(status, e.status) + except Exception as e: + self.fail('Catch unexpected exception') + finally: + client.close_socket() + finally: + self._close_server(server) + + 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_permessage_deflate(self): + def test_function(client): + # From the examples in the spec. + compressed_hello = b'\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.assertEqual('permessage-deflate', parameter.name()) + self.assertEqual([], 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(b'\xf2\x48\xcd', + client_for_testing.OPCODE_TEXT, + end=False, + rsv1=1) + client._stream.send_data(b'\xc9\xc9\x07\x00', + client_for_testing.OPCODE_TEXT) + client._stream.assert_receive_binary( + b'\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.assertEqual('permessage-deflate', parameter.name()) + self.assertEqual([], 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(b'\xf2\x48\xcd\xc9\xc9\x07\x00', + client_for_testing.OPCODE_TEXT, + rsv1=1) + client._stream.send_data(b'\xf2\x00\x11\x00\x00', + client_for_testing.OPCODE_TEXT, + rsv1=1) + client._stream.assert_receive_binary( + b'\xf2\x48\xcd\xc9\xc9\x07\x00', + opcode=client_for_testing.OPCODE_TEXT, + rsv1=1) + client._stream.assert_receive_binary( + b'\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.assertEqual('permessage-deflate', parameter.name()) + self.assertEqual([], 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(b'\xf2\x48\xcd\xc9\xc9\x07\x00', + client_for_testing.OPCODE_TEXT, + rsv1=1) + client._stream.send_data(b'\xf2\x00\x11\x00\x00', + client_for_testing.OPCODE_TEXT, + rsv1=1) + client._stream.assert_receive_binary( + b'\xf2\x48\xcd\xc9\xc9\x07\x00', + opcode=client_for_testing.OPCODE_TEXT, + rsv1=1) + client._stream.assert_receive_binary( + b'\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.assertEqual('permessage-deflate', parameter.name()) + self.assertEqual([('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 = b'\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.assertEqual('permessage-deflate', parameter.name()) + self.assertEqual([], 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 = b'\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.assertEqual('permessage-deflate', parameter.name()) + self.assertEqual([('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_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(b'\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(b'\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(b'\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) + + 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_options(_echo_check_procedure, options) + + def test_invalid_absolute_uri(self): + """Tests invalid absolute uri request.""" + + options = self._options + options.resource = 'ws://invalidlocalhost:%d/echo' % options.server_port + options.server_stderr = subprocess.PIPE + + self._run_http_fallback_test(options, 404) + + 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_invalid_resource(self): + """Tests invalid resource path.""" + + options = self._options + options.resource = '/no_resource' + + self.server_stderr = subprocess.PIPE + self._run_http_fallback_test(options, 404) + + def test_fragmentized_resource(self): + """Tests resource name with fragment""" + + options = self._options + options.resource = '/echo#fragment' + + self.server_stderr = subprocess.PIPE + self._run_http_fallback_test(options, 400) + + 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) + + def test_basic_auth_connection(self): + """Test successful basic auth""" + + options = self._options + options.use_basic_auth = True + + self.server_stderr = subprocess.PIPE + self._run_test_with_options(_check_handshake_with_basic_auth, + options, + server_options=['--basic-auth']) + + def test_invalid_basic_auth_connection(self): + """Tests basic auth with invalid credentials""" + + options = self._options + options.use_basic_auth = True + options.basic_auth_credential = 'invalid:test' + + self.server_stderr = subprocess.PIPE + + with self.assertRaises(client_for_testing.HttpStatusException) as e: + self._run_test_with_options(_check_handshake_with_basic_auth, + options, + server_options=['--basic-auth']) + self.assertEqual(101, e.exception.status) + + +class EndToEndTestWithEchoClient(EndToEndTestBase): + def setUp(self): + EndToEndTestBase.setUp(self) + + def _check_example_echo_client_result(self, expected, stdoutdata, + stderrdata): + actual = stdoutdata.decode(locale.getpreferredencoding()) + + # In Python 3 on Windows we get "\r\n" terminators back from + # the subprocess and we need to replace them with "\n" to get + # a match. This is a bit of a hack, but avoids platform- and + # version- specific code. + actual = actual.replace('\r\n', '\n') + + 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 = (u'Send: Hello\n' + u'Recv: Hello\n' + u'Send: <>\n' + u'Recv: <>\n' + u'Send close\n' + u'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._close_server(server) + + +class EndToEndTestWithCgi(EndToEndTestBase): + def setUp(self): + EndToEndTestBase.setUp(self) + + def test_cgi(self): + """Verifies that CGI scripts work.""" + + server = self._run_server(extra_args=['--cgi-paths', '/cgi-bin']) + time.sleep(_SERVER_WARMUP_IN_SEC) + + url = 'http://localhost:%d/cgi-bin/hi.py' % self._options.server_port + + # urlopen() in Python 2.7 doesn't support "with". + try: + f = urllib.request.urlopen(url) + except: + self._close_server(server) + raise + + try: + self.assertEqual(f.getcode(), 200) + self.assertEqual(f.info().get('Content-Type'), 'text/plain') + body = f.read() + self.assertEqual(body.rstrip(b'\r\n'), b'Hi from hi.py') + finally: + f.close() + self._close_server(server) + + +if __name__ == '__main__': + unittest.main() + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_extensions.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_extensions.py new file mode 100755 index 0000000000..39a111888b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_extensions.py @@ -0,0 +1,192 @@ +#!/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.""" + +from __future__ import absolute_import +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 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())) + + +if __name__ == '__main__': + unittest.main() + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake.py new file mode 100755 index 0000000000..7f4acf56ff --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake.py @@ -0,0 +1,172 @@ +#!/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.""" + +from __future__ import absolute_import +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/third_party/pywebsocket3/test/test_handshake_hybi.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake_hybi.py new file mode 100755 index 0000000000..8c65822170 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_handshake_hybi.py @@ -0,0 +1,422 @@ +#!/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.""" + +from __future__ import absolute_import +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 + +from test 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(b'') + 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 = ( + b'HTTP/1.1 101 Switching Protocols\r\n' + b'Upgrade: websocket\r\n' + b'Connection: Upgrade\r\n' + b'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 = ( + b'HTTP/1.1 101 Switching Protocols\r\n' + b'Upgrade: websocket\r\n' + b'Connection: Upgrade\r\n' + b'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n' + b'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-deflate; server_no_context_takeover') + + EXPECTED_RESPONSE = ( + b'HTTP/1.1 101 Switching Protocols\r\n' + b'Upgrade: websocket\r\n' + b'Connection: Upgrade\r\n' + b'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n' + b'Sec-WebSocket-Extensions: ' + b'permessage-deflate; server_no_context_takeover\r\n' + b'\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_DEFLATE_EXTENSION, extension.name()) + self.assertEqual(['server_no_context_takeover'], + extension.get_parameter_names()) + self.assertEqual( + None, extension.get_parameter_value('server_no_context_takeover')) + self.assertEqual(1, len(request.ws_extension_processors)) + self.assertEqual('deflate', 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-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-deflate', first_extension.name()) + 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_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 as e: + self.assertTrue(expect_handshake_exception) + self.assertEqual(expected_status, e.status) + except VersionException as 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/third_party/pywebsocket3/test/test_http_header_util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_http_header_util.py new file mode 100755 index 0000000000..f8c8e7a981 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_http_header_util.py @@ -0,0 +1,93 @@ +#!/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.""" + +from __future__ import absolute_import +import unittest +import sys + +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:INVALID_PORT') + self.assertEqual(None, resource) + + host, port, resource = http_header_util.parse_uri( + 'ws://localhost:-1/ws') + if sys.hexversion >= 0x030600f0: + self.assertEqual(None, resource) + else: + self.assertEqual('localhost', host) + self.assertEqual(80, port) + self.assertEqual('/ws', resource) + + +if __name__ == '__main__': + unittest.main() + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_memorizingfile.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_memorizingfile.py new file mode 100755 index 0000000000..f7288c510b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_memorizingfile.py @@ -0,0 +1,100 @@ +#!/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.""" + +from __future__ import absolute_import +import unittest +import six + +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( + six.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( + six.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(six.StringIO('')) + self.check(memorizing_file, 10, []) + + def test_get_memorized_lines_with_size(self): + for size in range(1, 10): + memorizing_file = memorizingfile.MemorizingFile( + six.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/third_party/pywebsocket3/test/test_mock.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_mock.py new file mode 100755 index 0000000000..073873dde9 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_mock.py @@ -0,0 +1,137 @@ +#!/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.""" + +from __future__ import absolute_import +import six.moves.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(b'ABC\r\nDEFG\r\n\r\nHIJK') + + def test_readline(self): + self.assertEqual(b'ABC\r\n', self._conn.readline()) + self.assertEqual(b'DEFG\r\n', self._conn.readline()) + self.assertEqual(b'\r\n', self._conn.readline()) + self.assertEqual(b'HIJK', self._conn.readline()) + self.assertEqual(b'', self._conn.readline()) + + def test_read(self): + self.assertEqual(b'ABC\r\nD', self._conn.read(6)) + self.assertEqual(b'EFG\r\n\r\nHI', self._conn.read(9)) + self.assertEqual(b'JK', self._conn.read(10)) + self.assertEqual(b'', self._conn.read(10)) + + def test_read_and_readline(self): + self.assertEqual(b'ABC\r\nD', self._conn.read(6)) + self.assertEqual(b'EFG\r\n', self._conn.readline()) + self.assertEqual(b'\r\nHIJK', self._conn.read(9)) + self.assertEqual(b'', self._conn.readline()) + + def test_write(self): + self._conn.write(b'Hello\r\n') + self._conn.write(b'World\r\n') + self.assertEqual(b'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 = six.moves.queue.Queue() + reader = LineReader(conn, queue) + self.assertTrue(queue.empty()) + conn.put_bytes(b'Foo bar\r\n') + read = queue.get() + self.assertEqual(b'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/third_party/pywebsocket3/test/test_msgutil.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_msgutil.py new file mode 100755 index 0000000000..1122c281b7 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_msgutil.py @@ -0,0 +1,912 @@ +#!/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.""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +import array +import six.moves.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 PerMessageDeflateExtensionProcessor +from mod_pywebsocket import msgutil +from mod_pywebsocket.stream import InvalidUTF8Exception +from mod_pywebsocket.stream import Stream +from mod_pywebsocket.stream import StreamOptions +from mod_pywebsocket import util +from test import mock +from six.moves import map +from six.moves import range +from six import iterbytes + +# We use one fixed nonce for testing instead of cryptographically secure PRNG. +_MASKING_NONCE = b'ABCD' + + +def _mask_hybi(frame): + if isinstance(frame, six.text_type): + Exception('masking does not accept Texts') + + frame_key = list(iterbytes(_MASKING_NONCE)) + frame_key_len = len(frame_key) + result = bytearray(frame) + count = 0 + + for i in range(len(result)): + result[i] ^= frame_key[count] + count = (count + 1) % frame_key_len + + return _MASKING_NONCE + bytes(result) + + +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, permessage_deflate_request=None): + req = mock.MockRequest(connection=mock.MockConn(read_data)) + req.ws_version = common.VERSION_HYBI_LATEST + req.ws_extension_processors = [] + + processor = None + if 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(b''.join(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 + + +class BasicMessageTest(unittest.TestCase): + """Basic tests for Stream.""" + def test_send_message(self): + request = _create_request() + msgutil.send_message(request, 'Hello') + self.assertEqual(b'\x81\x05Hello', request.connection.written_data()) + + payload = 'a' * 125 + request = _create_request() + msgutil.send_message(request, payload) + self.assertEqual(b'\x81\x7d' + payload.encode('UTF-8'), + request.connection.written_data()) + + def test_send_medium_message(self): + payload = 'a' * 126 + request = _create_request() + msgutil.send_message(request, payload) + self.assertEqual(b'\x81\x7e\x00\x7e' + payload.encode('UTF-8'), + request.connection.written_data()) + + payload = 'a' * ((1 << 16) - 1) + request = _create_request() + msgutil.send_message(request, payload) + self.assertEqual(b'\x81\x7e\xff\xff' + payload.encode('UTF-8'), + request.connection.written_data()) + + def test_send_large_message(self): + payload = 'a' * (1 << 16) + request = _create_request() + msgutil.send_message(request, payload) + self.assertEqual( + b'\x81\x7f\x00\x00\x00\x00\x00\x01\x00\x00' + + payload.encode('UTF-8'), 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(b'\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(b'\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(b'\x01\x0cHello World!\x80\x00', + request.connection.written_data()) + + def test_receive_message(self): + request = _create_request((b'\x81\x85', b'Hello'), + (b'\x81\x86', b'World!')) + self.assertEqual('Hello', msgutil.receive_message(request)) + self.assertEqual('World!', msgutil.receive_message(request)) + + payload = b'a' * 125 + request = _create_request((b'\x81\xfd', payload)) + self.assertEqual(payload.decode('UTF-8'), + msgutil.receive_message(request)) + + def test_receive_medium_message(self): + payload = b'a' * 126 + request = _create_request((b'\x81\xfe\x00\x7e', payload)) + self.assertEqual(payload.decode('UTF-8'), + msgutil.receive_message(request)) + + payload = b'a' * ((1 << 16) - 1) + request = _create_request((b'\x81\xfe\xff\xff', payload)) + self.assertEqual(payload.decode('UTF-8'), + msgutil.receive_message(request)) + + def test_receive_large_message(self): + payload = b'a' * (1 << 16) + request = _create_request( + (b'\x81\xff\x00\x00\x00\x00\x00\x01\x00\x00', payload)) + self.assertEqual(payload.decode('UTF-8'), + 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 = b'a' + # 1 byte can be represented without extended payload length field. + request = _create_request( + (b'\x81\xff\x00\x00\x00\x00\x00\x00\x00\x01', payload)) + self.assertEqual(payload.decode('UTF-8'), + msgutil.receive_message(request)) + + def test_receive_message_unicode(self): + request = _create_request((b'\x81\x83', b'\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((b'\x81\x82', b'\x80\x81')) + # Invalid characters should raise InvalidUTF8Exception + self.assertRaises(InvalidUTF8Exception, msgutil.receive_message, + request) + + def test_receive_fragments(self): + request = _create_request((b'\x01\x85', b'Hello'), (b'\x00\x81', b' '), + (b'\x00\x85', b'World'), (b'\x80\x81', b'!')) + 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((b'\x01\x82', b'\xe6\xbc'), + (b'\x00\x82', b'\xa2\xe5'), + (b'\x80\x82', b'\xad\x97')) + self.assertEqual(u'\u6f22\u5b57', msgutil.receive_message(request)) + + def test_receive_fragments_immediate_zero_termination(self): + request = _create_request((b'\x01\x8c', b'Hello World!'), + (b'\x80\x80', b'')) + self.assertEqual('Hello World!', msgutil.receive_message(request)) + + def test_receive_fragments_duplicate_start(self): + request = _create_request((b'\x01\x85', b'Hello'), + (b'\x01\x85', b'World')) + self.assertRaises(msgutil.InvalidFrameException, + msgutil.receive_message, request) + + def test_receive_fragments_intermediate_but_not_started(self): + request = _create_request((b'\x00\x85', b'Hello')) + self.assertRaises(msgutil.InvalidFrameException, + msgutil.receive_message, request) + + def test_receive_fragments_end_but_not_started(self): + request = _create_request((b'\x80\x85', b'Hello')) + self.assertRaises(msgutil.InvalidFrameException, + msgutil.receive_message, request) + + def test_receive_message_discard(self): + request = _create_request( + (b'\x8f\x86', b'IGNORE'), (b'\x81\x85', b'Hello'), + (b'\x8f\x89', b'DISREGARD'), (b'\x81\x86', b'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( + (b'\x88\x8a', struct.pack('!H', 1000) + b'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( + (b'\x88\xfd', struct.pack('!H', common.STATUS_NORMAL_CLOSURE) + + reason.encode('UTF-8'))) + 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(b'\x89\x0cHello World!', + request.connection.written_data()) + + def test_send_longest_ping(self): + request = _create_request() + msgutil.send_ping(request, 'a' * 125) + self.assertEqual(b'\x89\x7d' + b'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((b'\x89\x85', b'Hello'), + (b'\x81\x85', b'World')) + self.assertEqual('World', msgutil.receive_message(request)) + self.assertEqual(b'\x8a\x05Hello', request.connection.written_data()) + + request = _create_request((b'\x89\x85', b'Hello'), + (b'\x81\x85', b'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((b'\x89\xfd', b'a' * 125), + (b'\x81\x85', b'World')) + self.assertEqual('World', msgutil.receive_message(request)) + self.assertEqual(b'\x8a\x7d' + b'a' * 125, + request.connection.written_data()) + + def test_receive_ping_too_long(self): + request = _create_request((b'\x89\xfe\x00\x7e', b'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((b'\x8a\x85', b'Hello'), + (b'\x81\x85', b'World')) + request.on_pong_handler = handler + msgutil.send_ping(request, 'Hello') + self.assertEqual(b'\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(b'\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((b'\x8a\x85', b'Hello'), + (b'\x81\x85', b'World')) + msgutil.receive_message(request) + + request = _create_request((b'\x8a\x85', b'Hello'), + (b'\x81\x85', b'World')) + msgutil.send_ping(request, 'Jumbo') + # Body mismatch. + msgutil.receive_message(request) + + def test_ping_cannot_be_fragmented(self): + request = _create_request((b'\x09\x85', b'Hello')) + self.assertRaises(msgutil.InvalidFrameException, + msgutil.receive_message, request) + + def test_ping_with_too_long_payload(self): + request = _create_request((b'\x89\xfe\x01\x00', b'a' * 256)) + self.assertRaises(msgutil.InvalidFrameException, + msgutil.receive_message, request) + + +class PerMessageDeflateTest(unittest.TestCase): + """Tests for permessage-deflate extension.""" + def test_response_parameters(self): + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + extension.add_parameter('server_no_context_takeover', None) + processor = PerMessageDeflateExtensionProcessor(extension) + response = processor.get_extension_response() + self.assertTrue(response.has_parameter('server_no_context_takeover')) + self.assertEqual( + None, response.get_parameter_value('server_no_context_takeover')) + + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + extension.add_parameter('client_max_window_bits', None) + processor = PerMessageDeflateExtensionProcessor(extension) + + processor.set_client_max_window_bits(8) + processor.set_client_no_context_takeover(True) + response = processor.get_extension_response() + self.assertEqual( + '8', response.get_parameter_value('client_max_window_bits')) + self.assertTrue(response.has_parameter('client_no_context_takeover')) + self.assertEqual( + None, response.get_parameter_value('client_no_context_takeover')) + + def test_send_message(self): + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + request = _create_request_from_rawdata( + b'', 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(b'Hello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello = compressed_hello[:-4] + expected = b'\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( + b'', permessage_deflate_request=extension) + + msgutil.send_message(request, '') + + # Payload in binary: 0b00000000 + # From LSB, + # - 1 bit of BFINAL (0) + # - 2 bits of BTYPE (no compression) + # - 5 bits of padding + self.assertEqual(b'\xc1\x01\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( + b'', 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(b'\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( + b'', 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 = b'' + + compressed_hello = compress.compress(b'Hello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello = compressed_hello[:-4] + expected += b'\xc1%c' % len(compressed_hello) + expected += compressed_hello + + compressed_world = compress.compress(b'World') + compressed_world += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_world = compressed_world[:-4] + expected += b'\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( + b'', 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(b'Hello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + expected = b'\x41%c' % len(compressed_hello) + expected += compressed_hello + compressed_goodbye = compress.compress(b'Goodbye') + compressed_goodbye += compress.flush(zlib.Z_SYNC_FLUSH) + expected += b'\x00%c' % len(compressed_goodbye) + expected += compressed_goodbye + compressed_world = compress.compress(b'World') + compressed_world += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_world = compressed_world[:-4] + expected += b'\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( + b'', 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(b'') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + expected = b'\x41%c' % len(compressed_hello) + expected += compressed_hello + compressed_empty = compress.compress(b'Hello') + compressed_empty += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_empty = compressed_empty[:-4] + expected += b'\x80%c' % len(compressed_empty) + expected += compressed_empty + 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( + b'', 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(b'Hello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + expected = b'\x41%c' % len(compressed_hello) + expected += compressed_hello + compressed_empty = compress.compress(b'') + compressed_empty += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_empty = compressed_empty[:-4] + expected += b'\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( + b'', 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(b'\xc1%c' % expected_websocket_payload_size, + actual_header) + decompress = zlib.decompressobj(-8) + decompressed_message = decompress.decompress(actual_payload + + b'\x00\x00\xff\xff') + decompressed_message += decompress.flush() + self.assertEqual(test_message, decompressed_message.decode('UTF-8')) + 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( + b'', permessage_deflate_request=extension) + for i in range(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(b'Hello') + first_hello += compress.flush(zlib.Z_SYNC_FLUSH) + expected = b'\x41%c' % len(first_hello) + expected += first_hello + second_hello = compress.compress(b'Hello') + second_hello += compress.flush(zlib.Z_SYNC_FLUSH) + second_hello = second_hello[:-4] + expected += b'\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( + b'', permessage_deflate_request=extension) + self.assertEqual(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 = b'' + + compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, + -zlib.MAX_WBITS) + compressed_hello = compress.compress(b'Hello') + compressed_hello += compress.flush(zlib.Z_FINISH) + compressed_hello = compressed_hello + struct.pack('!B', 0) + expected += b'\x41%c' % len(compressed_hello) + expected += compressed_hello + + compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, + -zlib.MAX_WBITS) + compressed_world = compress.compress(b'World') + compressed_world += compress.flush(zlib.Z_FINISH) + compressed_world = compressed_world + struct.pack('!B', 0) + expected += b'\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(b'Hello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello = compressed_hello[:-4] + data = b'\xc1%c' % (len(compressed_hello) | 0x80) + data += _mask_hybi(compressed_hello) + + # Close frame + data += b'\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + b'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 = b''.join( + [struct.pack('!B', random.randint(0, 255)) for i in range(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 = b'' + 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 += b'%c%c' % (first_octet, chunk_size | 0x80) + data += _mask_hybi(chunk) + + frame_count += 1 + + self.assertTrue(len(chunk_sizes) > 10) + + # Close frame + data += b'\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + b'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 = b'' + + compressed_hello = compress.compress(b'HelloWebSocket') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello = compressed_hello[:-4] + split_position = len(compressed_hello) // 2 + data += b'\x41%c' % (split_position | 0x80) + data += _mask_hybi(compressed_hello[:split_position]) + + data += b'\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(b'World') + compressed_world += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_world = compressed_world[:-4] + data += b'\xc1%c' % (len(compressed_world) | 0x80) + data += _mask_hybi(compressed_world) + + # Close frame + data += b'\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + b'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 = b''.join( + [struct.pack('!B', random.randint(0, 255)) for i in range(1000)]) + + compress = None + + # Fragment the compressed payload into lots of frames. + bytes_chunked = 0 + compressed_payload = b'' + + 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 + + self.assertTrue(len(chunk_sizes) > 10) + self.assertTrue(sync_used) + self.assertTrue(finish_used) + + self.assertTrue(125 < len(compressed_payload)) + self.assertTrue(len(compressed_payload) < 65536) + data = b'\xc2\xfe' + struct.pack('!H', len(compressed_payload)) + data += _mask_hybi(compressed_payload) + + # Close frame + data += b'\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + b'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 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(b'\x81\x86' + _mask_hybi(b'Hello!')) + self.assertEqual('Hello!', receiver.receive()) + + def test_onmessage(self): + onmessage_queue = six.moves.queue.Queue() + + def onmessage_handler(message): + onmessage_queue.put(message) + + request = _create_blocking_request() + receiver = msgutil.MessageReceiver(request, onmessage_handler) + + request.connection.put_bytes(b'\x81\x86' + _mask_hybi(b'Hello!')) + 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(b'\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 = six.moves.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(b'\x81\x05Hello', send_queue.get()) + self.assertEqual(b'\x81\x05World', send_queue.get()) + + +if __name__ == '__main__': + unittest.main() + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_stream.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_stream.py new file mode 100755 index 0000000000..153899d205 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_stream.py @@ -0,0 +1,70 @@ +#!/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.""" + +from __future__ import absolute_import +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(b'\xf1\x81', header) + + # Maximum payload size + header = stream.create_header(common.OPCODE_TEXT, (1 << 63) - 1, 0, 0, + 0, 0, 0) + self.assertEqual(b'\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/third_party/pywebsocket3/test/test_util.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_util.py new file mode 100755 index 0000000000..bf4bd32bba --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/test_util.py @@ -0,0 +1,191 @@ +#!/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.""" + +from __future__ import absolute_import +from __future__ import print_function +import os +import random +import sys +import unittest +import struct + +import set_sys_path # Update sys.path to locate mod_pywebsocket module. + +from mod_pywebsocket import util +from six.moves import range +from six import PY3 +from six import int2byte + +_TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), 'testdata') + + +class UtilTest(unittest.TestCase): + """A unittest for util module.""" + 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(b'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(b'\xff\xff\xff\xff') + result = masker.mask(b'\xe6\x97\xa5') + self.assertEqual(b'\x19\x68\x5a', result) + + masker = util.RepeatedXorMasker(b'\x00\x00\x00\x00') + result = masker.mask(b'\xe6\x97\xa5') + self.assertEqual(b'\xe6\x97\xa5', result) + + masker = util.RepeatedXorMasker(b'\xe6\x97\xa5\x20') + result = masker.mask(b'\xe6\x97\xa5') + self.assertEqual(b'\x00\x00\x00', result) + + def test_mask_twice(self): + masker = util.RepeatedXorMasker(b'\x00\x7f\xff\x20') + # mask[0], mask[1], ... will be used. + result = masker.mask(b'\x00\x00\x00\x00\x00') + self.assertEqual(b'\x00\x7f\xff\x20\x00', result) + # mask[2], mask[0], ... will be used for the next call. + result = masker.mask(b'\x00\x00\x00\x00\x00') + self.assertEqual(b'\x7f\xff\x20\x00\x7f', result) + + def test_mask_large_data(self): + masker = util.RepeatedXorMasker(b'mASk') + original = b''.join([util.pack_byte(i % 256) for i in range(1000)]) + result = masker.mask(original) + expected = b''.join([ + util.pack_byte((i % 256) ^ ord('mASk'[i % 4])) for i in range(1000) + ]) + self.assertEqual(expected, result) + + masker = util.RepeatedXorMasker(b'MaSk') + first_part = b'The WebSocket Protocol enables two-way communication.' + result = masker.mask(first_part) + self.assertEqual( + b'\x19\t6K\x1a\x0418"\x028\x0e9A\x03\x19"\x15<\x08"\rs\x0e#' + b'\x001\x07(\x12s\x1f:\x0e~\x1c,\x18s\x08"\x0c>\x1e#\x080\n9' + b'\x08<\x05c', result) + second_part = b'It has two parts: a handshake and the data transfer.' + result = masker.mask(second_part) + self.assertEqual( + b"('K%\x00 K9\x16<K=\x00!\x1f>[s\nm\t2\x05)\x12;\n&\x04s\n#" + b"\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' + b'-' * 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 = b''.join( + [int2byte(random.randint(0, 255)) for i in range(100 * 1024)]) + + chunked_input = get_random_section(source, 10) + + deflater = util._Deflater(15) + compressed = [] + for chunk in chunked_input: + compressed.append(deflater.compress(chunk)) + compressed.append(deflater.compress_and_finish(b'')) + + chunked_expectation = get_random_section(source, 10) + + inflater = util._Inflater(15) + inflater.append(b''.join(compressed)) + for chunk in chunked_expectation: + decompressed = inflater.decompress(len(chunk)) + self.assertEqual(chunk, decompressed) + + self.assertEqual(b'', inflater.decompress(-1)) + + +if __name__ == '__main__': + unittest.main() + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/README b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/README new file mode 100644 index 0000000000..c001aa5595 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/README @@ -0,0 +1 @@ +Test data directory diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/abort_by_user_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/abort_by_user_wsh.py new file mode 100644 index 0000000000..63cb541bb7 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/abort_by_user_wsh.py @@ -0,0 +1,41 @@ +# 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/third_party/pywebsocket3/test/testdata/handlers/blank_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/blank_wsh.py new file mode 100644 index 0000000000..b398e96778 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/blank_wsh.py @@ -0,0 +1,30 @@ +# 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/third_party/pywebsocket3/test/testdata/handlers/origin_check_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/origin_check_wsh.py new file mode 100644 index 0000000000..bf6442e65b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/origin_check_wsh.py @@ -0,0 +1,43 @@ +# 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): + message = 'origin_check_wsh.py is called for %s, %s' % ( + request.ws_resource, request.ws_protocol) + request.connection.write(message.encode('UTF-8')) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/exception_in_transfer_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/exception_in_transfer_wsh.py new file mode 100644 index 0000000000..54390994d7 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/exception_in_transfer_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. +"""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/third_party/pywebsocket3/test/testdata/handlers/sub/no_wsh_at_the_end.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/no_wsh_at_the_end.py new file mode 100644 index 0000000000..ebfddb7449 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/no_wsh_at_the_end.py @@ -0,0 +1,43 @@ +# 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): + message = 'sub/no_wsh_at_the_end.py is called for %s, %s' % ( + request.ws_resource, request.ws_protocol) + request.connection.write(message.encode('UTF-8')) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/non_callable_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/non_callable_wsh.py new file mode 100644 index 0000000000..8afcfa97a9 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/non_callable_wsh.py @@ -0,0 +1,35 @@ +# 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/third_party/pywebsocket3/test/testdata/handlers/sub/plain_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/plain_wsh.py new file mode 100644 index 0000000000..8a7db1e5ac --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/plain_wsh.py @@ -0,0 +1,41 @@ +# 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): + message = 'sub/plain_wsh.py is called for %s, %s' % (request.ws_resource, + request.ws_protocol) + request.connection.write(message.encode('UTF-8')) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py new file mode 100644 index 0000000000..cebb0da1ab --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py @@ -0,0 +1,43 @@ +# 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): + message = 'sub/wrong_handshake_sig_wsh.py is called for %s, %s' % ( + request.ws_resource, request.ws_protocol) + request.connection.write(message.encode('UTF-8')) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py new file mode 100644 index 0000000000..ad27d6bcfe --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py @@ -0,0 +1,43 @@ +# 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): + message = 'sub/wrong_transfer_sig_wsh.py is called for %s, %s' % ( + request.ws_resource, request.ws_protocol) + request.connection.write(message.encode('UTF-8')) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/hello.pl b/testing/web-platform/tests/tools/third_party/pywebsocket3/test/testdata/hello.pl new file mode 100644 index 0000000000..882ef5a100 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pywebsocket3/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"; |