diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /testing/web-platform/tests/tools/pywebsocket | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'testing/web-platform/tests/tools/pywebsocket')
88 files changed, 22152 insertions, 0 deletions
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/COPYING b/testing/web-platform/tests/tools/pywebsocket/src/COPYING new file mode 100644 index 000000000..989d02e4c --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/COPYING @@ -0,0 +1,28 @@ +Copyright 2012, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/testing/web-platform/tests/tools/pywebsocket/src/MANIFEST.in b/testing/web-platform/tests/tools/pywebsocket/src/MANIFEST.in new file mode 100644 index 000000000..19256882c --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/MANIFEST.in @@ -0,0 +1,6 @@ +include COPYING +include MANIFEST.in +include README +recursive-include example *.py +recursive-include mod_pywebsocket *.py +recursive-include test *.py diff --git a/testing/web-platform/tests/tools/pywebsocket/src/README b/testing/web-platform/tests/tools/pywebsocket/src/README new file mode 100644 index 000000000..c8c758f5e --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/README @@ -0,0 +1,17 @@ +INSTALL + +To install this package to the system, run this: +$ python setup.py build +$ sudo python setup.py install + +To install this package as a normal user, run this instead: +$ python setup.py build +$ python setup.py install --user + +LAUNCH + +To use pywebsocket as Apache module, run this to read the document: +$ pydoc mod_pywebsocket + +To use pywebsocket as standalone server, run this to read the document: +$ pydoc mod_pywebsocket.standalone diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/abort_handshake_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/abort_handshake_wsh.py new file mode 100644 index 000000000..008023a1f --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/abort_handshake_wsh.py @@ -0,0 +1,43 @@ +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +from mod_pywebsocket import handshake + + +def web_socket_do_extra_handshake(request): + raise handshake.AbortedByUserException( + "Aborted in web_socket_do_extra_handshake") + + +def web_socket_transfer_data(request): + pass + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/abort_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/abort_wsh.py new file mode 100644 index 000000000..2bbf005f6 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/abort_wsh.py @@ -0,0 +1,43 @@ +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +from mod_pywebsocket import handshake + + +def web_socket_do_extra_handshake(request): + pass + + +def web_socket_transfer_data(request): + raise handshake.AbortedByUserException( + "Aborted in web_socket_transfer_data") + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/arraybuffer_benchmark.html b/testing/web-platform/tests/tools/pywebsocket/src/example/arraybuffer_benchmark.html new file mode 100644 index 000000000..869cd7e1e --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/arraybuffer_benchmark.html @@ -0,0 +1,134 @@ +<!-- +Copyright 2013, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--> + +<html> +<head> +<title>ArrayBuffer benchmark</title> +<script src="util.js"></script> +<script> +var PRINT_SIZE = true; + +// Initial size of arrays. +var START_SIZE = 10 * 1024; +// Stops benchmark when the size of an array exceeds this threshold. +var STOP_THRESHOLD = 100000 * 1024; +// If the size of each array is small, write/read the array multiple times +// until the sum of sizes reaches this threshold. +var MIN_TOTAL = 100000 * 1024; +var MULTIPLIERS = [5, 2]; + +// Repeat benchmark for several times to measure performance of optimized +// (such as JIT) run. +var REPEAT_FOR_WARMUP = 3; + +function writeBenchmark(size, minTotal) { + var totalSize = 0; + while (totalSize < minTotal) { + var arrayBuffer = new ArrayBuffer(size); + + // Write 'a's. + fillArrayBuffer(arrayBuffer, 0x61); + + totalSize += size; + } + return totalSize; +} + +function readBenchmark(size, minTotal) { + var totalSize = 0; + while (totalSize < minTotal) { + var arrayBuffer = new ArrayBuffer(size); + + if (!verifyArrayBuffer(arrayBuffer, 0x00)) { + queueLog('Verification failed'); + return -1; + } + + totalSize += size; + } + return totalSize; +} + +function runBenchmark(benchmarkFunction, + size, + stopThreshold, + minTotal, + multipliers, + multiplierIndex) { + while (size <= stopThreshold) { + var maxSpeed = 0; + + for (var i = 0; i < REPEAT_FOR_WARMUP; ++i) { + var startTimeInMs = getTimeStamp(); + + var totalSize = benchmarkFunction(size, minTotal); + + maxSpeed = Math.max(maxSpeed, + calculateSpeedInKB(totalSize, startTimeInMs)); + } + queueLog(formatResultInKiB(size, maxSpeed, PRINT_SIZE)); + + size *= multipliers[multiplierIndex]; + multiplierIndex = (multiplierIndex + 1) % multipliers.length; + } +} + +function runBenchmarks() { + queueLog('Message size in KiB, Speed in kB/s'); + + queueLog('Write benchmark'); + runBenchmark( + writeBenchmark, START_SIZE, STOP_THRESHOLD, MIN_TOTAL, MULTIPLIERS, 0); + queueLog('Finished'); + + queueLog('Read benchmark'); + runBenchmark( + readBenchmark, START_SIZE, STOP_THRESHOLD, MIN_TOTAL, MULTIPLIERS, 0); + addToLog('Finished'); +} + +function init() { + logBox = document.getElementById('log'); + + queueLog(window.navigator.userAgent.toLowerCase()); + + addToLog('Started...'); + + setTimeout(runBenchmarks, 0); +} + +</script> +</head> +<body onload="init()"> +<textarea + id="log" rows="50" style="width: 100%" readonly></textarea> +</body> +</html> diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/bench_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/bench_wsh.py new file mode 100644 index 000000000..5067ca7d8 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/bench_wsh.py @@ -0,0 +1,60 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""A simple load tester for WebSocket clients. + +A client program sends a message formatted as "<time> <count> <message>" to +this handler. This handler starts sending total <count> WebSocket messages +containing <message> every <time> seconds. <time> can be a floating point +value. <count> must be an integer value. +""" + + +import time + + +def web_socket_do_extra_handshake(request): + pass # Always accept. + + +def web_socket_transfer_data(request): + line = request.ws_stream.receive_message() + parts = line.split(' ') + if len(parts) != 3: + raise ValueError('Bad parameter format') + wait = float(parts[0]) + count = int(parts[1]) + message = parts[2] + for i in xrange(count): + request.ws_stream.send_message(message) + time.sleep(wait) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark.html b/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark.html new file mode 100644 index 000000000..3a218173a --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark.html @@ -0,0 +1,203 @@ +<!-- +Copyright 2013, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--> + +<html> +<head> +<title>WebSocket benchmark</title> +<script src="util_main.js"></script> +<script src="util.js"></script> +<script src="benchmark.js"></script> +<script> +var addressBox = null; + +function getConfig() { + return { + prefixUrl: addressBox.value, + printSize: getBoolFromCheckBox('printsize'), + numSockets: getIntFromInput('numsockets'), + // Initial size of messages. + numIterations: getIntFromInput('numiterations'), + numWarmUpIterations: getIntFromInput('numwarmupiterations'), + startSize: getIntFromInput('startsize'), + // Stops benchmark when the size of message exceeds this threshold. + stopThreshold: getIntFromInput('stopthreshold'), + // If the size of each message is small, send/receive multiple messages + // until the sum of sizes reaches this threshold. + minTotal: getIntFromInput('mintotal'), + multipliers: getIntArrayFromInput('multipliers'), + verifyData: getBoolFromCheckBox('verifydata') + }; +} + +var worker = new Worker('benchmark.js'); +worker.onmessage = onMessage; + +function onSendBenchmark() { + var config = getConfig(); + + if (getBoolFromCheckBox('worker')) { + worker.postMessage({type: 'sendBenchmark', config: config}); + } else { + config.addToLog = addToLog; + config.addToSummary = addToSummary; + config.measureValue = measureValue; + sendBenchmark(config); + } +} + +function onReceiveBenchmark() { + var config = getConfig(); + + if (getBoolFromCheckBox('worker')) { + worker.postMessage({type: 'receiveBenchmark', config: config}); + } else { + config.addToLog = addToLog; + config.addToSummary = addToSummary; + config.measureValue = measureValue; + receiveBenchmark(config); + } +} + +function onBatchBenchmark() { + var config = getConfig(); + + if (getBoolFromCheckBox('worker')) { + worker.postMessage({type: 'batchBenchmark', config: config}); + } else { + config.addToLog = addToLog; + config.addToSummary = addToSummary; + config.measureValue = measureValue; + batchBenchmark(config); + } +} + +function onStop() { + var config = getConfig(); + + if (getBoolFromCheckBox('worker')) { + worker.postMessage({type: 'stop', config: config}); + } else { + config.addToLog = addToLog; + config.addToSummary = addToSummary; + config.measureValue = measureValue; + stop(config); + } +} +function init() { + addressBox = document.getElementById('address'); + logBox = document.getElementById('log'); + + summaryBox = document.getElementById('summary'); + + var scheme = window.location.protocol == 'https:' ? 'wss://' : 'ws://'; + var defaultAddress = scheme + window.location.host + '/benchmark_helper'; + + addressBox.value = defaultAddress; + + addToLog(window.navigator.userAgent.toLowerCase()); + addToSummary(window.navigator.userAgent.toLowerCase()); + + if (!('WebSocket' in window)) { + addToLog('WebSocket is not available'); + } +} +</script> +</head> +<body onload="init()"> + +<div id="benchmark_div"> + url <input type="text" id="address" size="40"> + <input type="button" value="send" onclick="onSendBenchmark()"> + <input type="button" value="receive" onclick="onReceiveBenchmark()"> + <input type="button" value="batch" onclick="onBatchBenchmark()"> + <input type="button" value="stop" onclick="onStop()"> + + <br/> + + <input type="checkbox" id="printsize" checked> + <label for="printsize">Print size and time per message</label> + <input type="checkbox" id="verifydata" checked> + <label for="verifydata">Verify data</label> + <input type="checkbox" id="worker"> + <label for="worker">Run on worker</label> + + <br/> + + Parameters: + + <br/> + + <table> + <tr> + <td>Num sockets</td> + <td><input type="text" id="numsockets" value="1"></td> + </tr> + <tr> + <td>Number of iterations</td> + <td><input type="text" id="numiterations" value="1"></td> + </tr> + <tr> + <td>Number of warm-up iterations</td> + <td><input type="text" id="numwarmupiterations" value="0"></td> + </tr> + <tr> + <td>Start size</td> + <td><input type="text" id="startsize" value="10240"></td> + </tr> + <tr> + <td>Stop threshold</td> + <td><input type="text" id="stopthreshold" value="102400000"></td> + </tr> + <tr> + <td>Minimum total</td> + <td><input type="text" id="mintotal" value="102400000"></td> + </tr> + <tr> + <td>Multipliers</td> + <td><input type="text" id="multipliers" value="5, 2"></td> + </tr> + </table> +</div> + +<div id="log_div"> + <textarea + id="log" rows="20" style="width: 100%" readonly></textarea> +</div> +<div id="summary_div"> + Summary + <textarea + id="summary" rows="20" style="width: 100%" readonly></textarea> +</div> + +Note: Effect of RTT is not eliminated. + +</body> +</html> diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark.js b/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark.js new file mode 100644 index 000000000..d347ae9e1 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark.js @@ -0,0 +1,309 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the COPYING file or at +// https://developers.google.com/open-source/licenses/bsd + +if (typeof importScripts !== "undefined") { + // Running on a worker + importScripts('util.js', 'util_worker.js'); +} + +// Namespace for holding globals. +var benchmark = {startTimeInMs: 0}; + +var sockets = []; +var numEstablishedSockets = 0; + +var timerID = null; + +function destroySocket(socket) { + socket.onopen = null; + socket.onmessage = null; + socket.onerror = null; + socket.onclose = null; + socket.close(); +} + +function destroyAllSockets() { + for (var i = 0; i < sockets.length; ++i) { + destroySocket(sockets[i]); + } + sockets = []; +} + +function sendBenchmarkStep(size, config) { + timerID = null; + + var totalSize = 0; + var totalReplied = 0; + + var onMessageHandler = function(event) { + if (!verifyAcknowledgement(config, event.data, size)) { + destroyAllSockets(); + return; + } + + totalReplied += size; + + if (totalReplied < totalSize) { + return; + } + + calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize); + + runNextTask(config); + }; + + for (var i = 0; i < sockets.length; ++i) { + var socket = sockets[i]; + socket.onmessage = onMessageHandler; + } + + var dataArray = []; + + while (totalSize < config.minTotal) { + var buffer = new ArrayBuffer(size); + + fillArrayBuffer(buffer, 0x61); + + dataArray.push(buffer); + totalSize += size; + } + + benchmark.startTimeInMs = getTimeStamp(); + + totalSize = 0; + + var socketIndex = 0; + var dataIndex = 0; + while (totalSize < config.minTotal) { + var command = ['send']; + command.push(config.verifyData ? '1' : '0'); + sockets[socketIndex].send(command.join(' ')); + sockets[socketIndex].send(dataArray[dataIndex]); + socketIndex = (socketIndex + 1) % sockets.length; + + totalSize += size; + ++dataIndex; + } +} + +function receiveBenchmarkStep(size, config) { + timerID = null; + + var totalSize = 0; + var totalReplied = 0; + + var onMessageHandler = function(event) { + var bytesReceived = event.data.byteLength; + if (bytesReceived != size) { + config.addToLog('Expected ' + size + 'B but received ' + + bytesReceived + 'B'); + destroyAllSockets(); + return; + } + + if (config.verifyData && !verifyArrayBuffer(event.data, 0x61)) { + config.addToLog('Response verification failed'); + destroyAllSockets(); + return; + } + + totalReplied += bytesReceived; + + if (totalReplied < totalSize) { + return; + } + + calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize); + + runNextTask(config); + }; + + for (var i = 0; i < sockets.length; ++i) { + var socket = sockets[i]; + socket.binaryType = 'arraybuffer'; + socket.onmessage = onMessageHandler; + } + + benchmark.startTimeInMs = getTimeStamp(); + + var socketIndex = 0; + while (totalSize < config.minTotal) { + sockets[socketIndex].send('receive ' + size); + socketIndex = (socketIndex + 1) % sockets.length; + + totalSize += size; + } +} + +function createSocket(config) { + // TODO(tyoshino): Add TCP warm up. + var url = config.prefixUrl; + + config.addToLog('Connect ' + url); + + var socket = new WebSocket(url); + socket.onmessage = function(event) { + config.addToLog('Unexpected message received. Aborting.'); + }; + socket.onerror = function() { + config.addToLog('Error'); + }; + socket.onclose = function(event) { + config.addToLog('Closed'); + }; + return socket; +} + +var tasks = []; + +function startBenchmark(config) { + clearTimeout(timerID); + destroyAllSockets(); + + numEstablishedSockets = 0; + + for (var i = 0; i < config.numSockets; ++i) { + var socket = createSocket(config); + socket.onopen = function() { + config.addToLog('Opened'); + + ++numEstablishedSockets; + + if (numEstablishedSockets == sockets.length) { + runNextTask(config); + } + }; + sockets.push(socket); + } +} + +function runNextTask(config) { + var task = tasks.shift(); + if (task == undefined) { + config.addToLog('Finished'); + destroyAllSockets(); + return; + } + timerID = setTimeout(task, 0); +} + +function buildLegendString(config) { + var legend = '' + if (config.printSize) + legend = 'Message size in KiB, Time/message in ms, '; + legend += 'Speed in kB/s'; + return legend; +} + +function getConfigString(config) { + return '(WebSocket' + + ', ' + (typeof importScripts !== "undefined" ? 'Worker' : 'Main') + + ', numSockets=' + config.numSockets + + ', numIterations=' + config.numIterations + + ', verifyData=' + config.verifyData + + ', minTotal=' + config.minTotal + + ', numWarmUpIterations=' + config.numWarmUpIterations + + ')'; +} + +function addTasks(config, stepFunc) { + for (var i = 0; + i < config.numWarmUpIterations + config.numIterations; ++i) { + // Ignore the first |config.numWarmUpIterations| iterations. + if (i == config.numWarmUpIterations) + addResultClearingTask(config); + + var multiplierIndex = 0; + for (var size = config.startSize; + size <= config.stopThreshold; + ++multiplierIndex) { + var task = stepFunc.bind( + null, + size, + config); + tasks.push(task); + size *= config.multipliers[ + multiplierIndex % config.multipliers.length]; + } + } +} + +function addResultReportingTask(config, title) { + tasks.push(function(){ + timerID = null; + config.addToSummary(title); + reportAverageData(config); + clearAverageData(); + runNextTask(config); + }); +} + +function addResultClearingTask(config) { + tasks.push(function(){ + timerID = null; + clearAverageData(); + runNextTask(config); + }); +} + +function sendBenchmark(config) { + config.addToLog('Send benchmark'); + config.addToLog(buildLegendString(config)); + + tasks = []; + clearAverageData(); + addTasks(config, sendBenchmarkStep); + addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config)); + startBenchmark(config); +} + +function receiveBenchmark(config) { + config.addToLog('Receive benchmark'); + config.addToLog(buildLegendString(config)); + + tasks = []; + clearAverageData(); + addTasks(config, receiveBenchmarkStep); + addResultReportingTask(config, + 'Receive Benchmark ' + getConfigString(config)); + startBenchmark(config); +} + +function batchBenchmark(config) { + config.addToLog('Batch benchmark'); + config.addToLog(buildLegendString(config)); + + tasks = []; + clearAverageData(); + addTasks(config, sendBenchmarkStep); + addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config)); + addTasks(config, receiveBenchmarkStep); + addResultReportingTask(config, 'Receive Benchmark ' + + getConfigString(config)); + startBenchmark(config); +} + +function stop(config) { + clearTimeout(timerID); + timerID = null; + config.addToLog('Stopped'); + destroyAllSockets(); +} + +onmessage = function (message) { + var config = message.data.config; + config.addToLog = workerAddToLog; + config.addToSummary = workerAddToSummary; + config.measureValue = workerMeasureValue; + if (message.data.type === 'sendBenchmark') + sendBenchmark(config); + else if (message.data.type === 'receiveBenchmark') + receiveBenchmark(config); + else if (message.data.type === 'batchBenchmark') + batchBenchmark(config); + else if (message.data.type === 'stop') + stop(config); +}; diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark_helper_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark_helper_wsh.py new file mode 100644 index 000000000..44ad0bfee --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/benchmark_helper_wsh.py @@ -0,0 +1,85 @@ +# Copyright 2013, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Handler for benchmark.html.""" + + +def web_socket_do_extra_handshake(request): + # Turn off compression. + request.ws_extension_processors = [] + + +def web_socket_transfer_data(request): + data = '' + + while True: + command = request.ws_stream.receive_message() + if command is None: + return + + if not isinstance(command, unicode): + raise ValueError('Invalid command data:' + command) + commands = command.split(' ') + if len(commands) == 0: + raise ValueError('Invalid command data: ' + command) + + if commands[0] == 'receive': + if len(commands) != 2: + raise ValueError( + 'Illegal number of arguments for send command' + + command) + size = int(commands[1]) + + # Reuse data if possible. + if len(data) != size: + data = 'a' * size + request.ws_stream.send_message(data, binary=True) + elif commands[0] == 'send': + if len(commands) != 2: + raise ValueError( + 'Illegal number of arguments for receive command' + + command) + verify_data = commands[1] == '1' + + data = request.ws_stream.receive_message() + if data is None: + raise ValueError('Payload not received') + size = len(data) + + if verify_data: + if data != 'a' * size: + raise ValueError('Payload verification failed') + + request.ws_stream.send_message(str(size)) + else: + raise ValueError('Invalid command: ' + commands[0]) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/close_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/close_wsh.py new file mode 100644 index 000000000..26b083840 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/close_wsh.py @@ -0,0 +1,69 @@ +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import struct + +from mod_pywebsocket import common +from mod_pywebsocket import stream + + +def web_socket_do_extra_handshake(request): + pass + + +def web_socket_transfer_data(request): + while True: + line = request.ws_stream.receive_message() + if line is None: + return + code, reason = line.split(' ', 1) + if code is None or reason is None: + return + request.ws_stream.close_connection(int(code), reason) + # close_connection() initiates closing handshake. It validates code + # and reason. If you want to send a broken close frame for a test, + # following code will be useful. + # > data = struct.pack('!H', int(code)) + reason.encode('UTF-8') + # > request.connection.write(stream.create_close_frame(data)) + # > # Suppress to re-respond client responding close frame. + # > raise Exception("customized server initiated closing handshake") + + +def web_socket_passive_closing_handshake(request): + # Simply echo a close status code + code, reason = request.ws_close_code, request.ws_close_reason + + # pywebsocket sets pseudo code for receiving an empty body close frame. + if code == common.STATUS_NO_STATUS_RECEIVED: + code = None + reason = '' + return code, reason + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/console.html b/testing/web-platform/tests/tools/pywebsocket/src/example/console.html new file mode 100644 index 000000000..ccd6d8f80 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/console.html @@ -0,0 +1,317 @@ +<!-- +Copyright 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--> + +<!-- +A simple console for testing WebSocket server. + +Type an address into the top text input and click connect to establish +WebSocket. Then, type some message into the bottom text input and click send +to send the message. Received/sent messages and connection state will be shown +on the middle textarea. +--> + +<html> +<head> +<title>WebSocket console</title> +<script> +var socket = null; + +var showTimeStamp = false; + +var addressBox = null; +var protocolsBox = null; +var logBox = null; +var messageBox = null; +var fileBox = null; +var codeBox = null; +var reasonBox = null; + +function getTimeStamp() { + return new Date().getTime(); +} + +function addToLog(log) { + if (showTimeStamp) { + logBox.value += '[' + getTimeStamp() + '] '; + } + logBox.value += log + '\n' + // Large enough to keep showing the latest message. + logBox.scrollTop = 1000000; +} + +function setbinarytype(binaryType) { + if (!socket) { + addToLog('Not connected'); + return; + } + + socket.binaryType = binaryType; + addToLog('Set binaryType to ' + binaryType); +} + +function send() { + if (!socket) { + addToLog('Not connected'); + return; + } + + socket.send(messageBox.value); + addToLog('> ' + messageBox.value); + messageBox.value = ''; +} + +function sendfile() { + if (!socket) { + addToLog('Not connected'); + return; + } + + var files = fileBox.files; + + if (files.length == 0) { + addToLog('File not selected'); + return; + } + + socket.send(files[0]); + addToLog('> Send ' + files[0].name); +} + +function parseProtocols(protocolsText) { + var protocols = protocolsText.split(','); + for (var i = 0; i < protocols.length; ++i) { + protocols[i] = protocols[i].trim(); + } + + if (protocols.length == 0) { + // Don't pass. + protocols = null; + } else if (protocols.length == 1) { + if (protocols[0].length == 0) { + // Don't pass. + protocols = null; + } else { + // Pass as a string. + protocols = protocols[0]; + } + } + + return protocols; +} + +function connect() { + var url = addressBox.value; + var protocols = parseProtocols(protocolsBox.value); + + if ('WebSocket' in window) { + if (protocols) { + socket = new WebSocket(url, protocols); + } else { + socket = new WebSocket(url); + } + } else { + return; + } + + socket.onopen = function () { + var extraInfo = []; + if (('protocol' in socket) && socket.protocol) { + extraInfo.push('protocol = ' + socket.protocol); + } + if (('extensions' in socket) && socket.extensions) { + extraInfo.push('extensions = ' + socket.extensions); + } + + var logMessage = 'Opened'; + if (extraInfo.length > 0) { + logMessage += ' (' + extraInfo.join(', ') + ')'; + } + addToLog(logMessage); + }; + socket.onmessage = function (event) { + if (('ArrayBuffer' in window) && (event.data instanceof ArrayBuffer)) { + addToLog('< Received an ArrayBuffer of ' + event.data.byteLength + + ' bytes') + } else if (('Blob' in window) && (event.data instanceof Blob)) { + addToLog('< Received a Blob of ' + event.data.size + ' bytes') + } else { + addToLog('< ' + event.data); + } + }; + socket.onerror = function () { + addToLog('Error'); + }; + socket.onclose = function (event) { + var logMessage = 'Closed ('; + if ((arguments.length == 1) && ('CloseEvent' in window) && + (event instanceof CloseEvent)) { + logMessage += 'wasClean = ' + event.wasClean; + // code and reason are present only for + // draft-ietf-hybi-thewebsocketprotocol-06 and later + if ('code' in event) { + logMessage += ', code = ' + event.code; + } + if ('reason' in event) { + logMessage += ', reason = ' + event.reason; + } + } else { + logMessage += 'CloseEvent is not available'; + } + addToLog(logMessage + ')'); + }; + + if (protocols) { + addToLog('Connect ' + url + ' (protocols = ' + protocols + ')'); + } else { + addToLog('Connect ' + url); + } +} + +function closeSocket() { + if (!socket) { + addToLog('Not connected'); + return; + } + + if (codeBox.value || reasonBox.value) { + socket.close(codeBox.value, reasonBox.value); + } else { + socket.close(); + } +} + +function printState() { + if (!socket) { + addToLog('Not connected'); + return; + } + + addToLog( + 'url = ' + socket.url + + ', readyState = ' + socket.readyState + + ', bufferedAmount = ' + socket.bufferedAmount); +} + +function init() { + var scheme = window.location.protocol == 'https:' ? 'wss://' : 'ws://'; + var defaultAddress = scheme + window.location.host + '/echo'; + + addressBox = document.getElementById('address'); + protocolsBox = document.getElementById('protocols'); + logBox = document.getElementById('log'); + messageBox = document.getElementById('message'); + fileBox = document.getElementById('file'); + codeBox = document.getElementById('code'); + reasonBox = document.getElementById('reason'); + + addressBox.value = defaultAddress; + + if (!('WebSocket' in window)) { + addToLog('WebSocket is not available'); + } +} +</script> +<style type="text/css"> +form { + margin: 0px; +} + +#connect_div, #log_div, #send_div, #sendfile_div, #close_div, #printstate_div { + padding: 5px; + margin: 5px; + border-width: 0px 0px 0px 10px; + border-style: solid; + border-color: silver; +} +</style> +</head> +<body onload="init()"> + +<div> + +<div id="connect_div"> + <form action="#" onsubmit="connect(); return false;"> + url <input type="text" id="address" size="40"> + <input type="submit" value="connect"> + <br/> + protocols <input type="text" id="protocols" size="20"> + </form> +</div> + +<div id="log_div"> + <textarea id="log" rows="10" cols="40" readonly></textarea> + <br/> + <input type="checkbox" + name="showtimestamp" + value="showtimestamp" + onclick="showTimeStamp = this.checked">Show time stamp +</div> + +<div id="send_div"> + <form action="#" onsubmit="send(); return false;"> + data <input type="text" id="message" size="40"> + <input type="submit" value="send"> + </form> +</div> + +<div id="sendfile_div"> + <form action="#" onsubmit="sendfile(); return false;"> + <input type="file" id="file" size="40"> + <input type="submit" value="send file"> + </form> + + Set binaryType + <input type="radio" + name="binarytype" + value="blob" + onclick="setbinarytype('blob')" checked>blob + <input type="radio" + name="binarytype" + value="arraybuffer" + onclick="setbinarytype('arraybuffer')">arraybuffer +</div> + +<div id="close_div"> + <form action="#" onsubmit="closeSocket(); return false;"> + code <input type="text" id="code" size="10"> + reason <input type="text" id="reason" size="20"> + <input type="submit" value="close"> + </form> +</div> + +<div id="printstate_div"> + <input type="button" value="print state" onclick="printState();"> +</div> + +</div> + +</body> +</html> diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/cookie_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/cookie_wsh.py new file mode 100644 index 000000000..8b327152e --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/cookie_wsh.py @@ -0,0 +1,32 @@ +# Copyright 2014 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the COPYING file or at +# https://developers.google.com/open-source/licenses/bsd + + +import urlparse + + +def _add_set_cookie(request, value): + request.extra_headers.append(('Set-Cookie', value)) + + +def web_socket_do_extra_handshake(request): + components = urlparse.urlparse(request.uri) + command = components[4] + + ONE_DAY_LIFE = 'Max-Age=86400' + + if command == 'set': + _add_set_cookie(request, '; '.join(['foo=bar', ONE_DAY_LIFE])) + elif command == 'set_httponly': + _add_set_cookie(request, + '; '.join(['httpOnlyFoo=bar', ONE_DAY_LIFE, 'httpOnly'])) + elif command == 'clear': + _add_set_cookie(request, 'foo=0; Max-Age=0') + _add_set_cookie(request, 'httpOnlyFoo=0; Max-Age=0') + + +def web_socket_transfer_data(request): + pass diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/echo_client.py b/testing/web-platform/tests/tools/pywebsocket/src/example/echo_client.py new file mode 100755 index 000000000..943ce64e8 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/echo_client.py @@ -0,0 +1,1128 @@ +#!/usr/bin/env python +# +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Simple WebSocket client named echo_client just because of historical reason. + +mod_pywebsocket directory must be in PYTHONPATH. + +Example Usage: + +# server setup + % cd $pywebsocket + % PYTHONPATH=$cwd/src python ./mod_pywebsocket/standalone.py -p 8880 \ + -d $cwd/src/example + +# run client + % PYTHONPATH=$cwd/src python ./src/example/echo_client.py -p 8880 \ + -s localhost \ + -o http://localhost -r /echo -m test + +or + +# run echo client to test IETF HyBi 00 protocol + run with --protocol-version=hybi00 +""" + + +import base64 +import codecs +import logging +from optparse import OptionParser +import os +import random +import re +import socket +import struct +import sys + +from mod_pywebsocket import common +from mod_pywebsocket.extensions import DeflateFrameExtensionProcessor +from mod_pywebsocket.extensions import PerMessageDeflateExtensionProcessor +from mod_pywebsocket.extensions import _PerMessageDeflateFramer +from mod_pywebsocket.extensions import _parse_window_bits +from mod_pywebsocket.stream import Stream +from mod_pywebsocket.stream import StreamHixie75 +from mod_pywebsocket.stream import StreamOptions +from mod_pywebsocket import util + + +_TIMEOUT_SEC = 10 +_UNDEFINED_PORT = -1 + +_UPGRADE_HEADER = 'Upgrade: websocket\r\n' +_UPGRADE_HEADER_HIXIE75 = 'Upgrade: WebSocket\r\n' +_CONNECTION_HEADER = 'Connection: Upgrade\r\n' + +# Special message that tells the echo server to start closing handshake +_GOODBYE_MESSAGE = 'Goodbye' + +_PROTOCOL_VERSION_HYBI13 = 'hybi13' +_PROTOCOL_VERSION_HYBI08 = 'hybi08' +_PROTOCOL_VERSION_HYBI00 = 'hybi00' +_PROTOCOL_VERSION_HIXIE75 = 'hixie75' + +# Constants for the --tls_module flag. +_TLS_BY_STANDARD_MODULE = 'ssl' +_TLS_BY_PYOPENSSL = 'pyopenssl' + +# Values used by the --tls-version flag. +_TLS_VERSION_SSL23 = 'ssl23' +_TLS_VERSION_SSL3 = 'ssl3' +_TLS_VERSION_TLS1 = 'tls1' + + +class ClientHandshakeError(Exception): + pass + + +def _build_method_line(resource): + return 'GET %s HTTP/1.1\r\n' % resource + + +def _origin_header(header, origin): + # 4.1 13. concatenation of the string "Origin:", a U+0020 SPACE character, + # and the /origin/ value, converted to ASCII lowercase, to /fields/. + return '%s: %s\r\n' % (header, origin.lower()) + + +def _format_host_header(host, port, secure): + # 4.1 9. Let /hostport/ be an empty string. + # 4.1 10. Append the /host/ value, converted to ASCII lowercase, to + # /hostport/ + hostport = host.lower() + # 4.1 11. If /secure/ is false, and /port/ is not 80, or if /secure/ + # is true, and /port/ is not 443, then append a U+003A COLON character + # (:) followed by the value of /port/, expressed as a base-ten integer, + # to /hostport/ + if ((not secure and port != common.DEFAULT_WEB_SOCKET_PORT) or + (secure and port != common.DEFAULT_WEB_SOCKET_SECURE_PORT)): + hostport += ':' + str(port) + # 4.1 12. concatenation of the string "Host:", a U+0020 SPACE + # character, and /hostport/, to /fields/. + return '%s: %s\r\n' % (common.HOST_HEADER, hostport) + + +def _receive_bytes(socket, length): + bytes = [] + remaining = length + while remaining > 0: + received_bytes = socket.recv(remaining) + if not received_bytes: + raise IOError( + 'Connection closed before receiving requested length ' + '(requested %d bytes but received only %d bytes)' % + (length, length - remaining)) + bytes.append(received_bytes) + remaining -= len(received_bytes) + return ''.join(bytes) + + +def _get_mandatory_header(fields, name): + """Gets the value of the header specified by name from fields. + + This function expects that there's only one header with the specified name + in fields. Otherwise, raises an ClientHandshakeError. + """ + + values = fields.get(name.lower()) + if values is None or len(values) == 0: + raise ClientHandshakeError( + '%s header not found: %r' % (name, values)) + if len(values) > 1: + raise ClientHandshakeError( + 'Multiple %s headers found: %r' % (name, values)) + return values[0] + + +def _validate_mandatory_header(fields, name, + expected_value, case_sensitive=False): + """Gets and validates the value of the header specified by name from + fields. + + If expected_value is specified, compares expected value and actual value + and raises an ClientHandshakeError on failure. You can specify case + sensitiveness in this comparison by case_sensitive parameter. This function + expects that there's only one header with the specified name in fields. + Otherwise, raises an ClientHandshakeError. + """ + + value = _get_mandatory_header(fields, name) + + if ((case_sensitive and value != expected_value) or + (not case_sensitive and value.lower() != expected_value.lower())): + raise ClientHandshakeError( + 'Illegal value for header %s: %r (expected) vs %r (actual)' % + (name, expected_value, value)) + + +class _TLSSocket(object): + """Wrapper for a TLS connection.""" + + def __init__(self, + raw_socket, tls_module, tls_version, disable_tls_compression): + self._logger = util.get_class_logger(self) + + if tls_module == _TLS_BY_STANDARD_MODULE: + if tls_version == _TLS_VERSION_SSL23: + version = ssl.PROTOCOL_SSLv23 + elif tls_version == _TLS_VERSION_SSL3: + version = ssl.PROTOCOL_SSLv3 + elif tls_version == _TLS_VERSION_TLS1: + version = ssl.PROTOCOL_TLSv1 + else: + raise ValueError( + 'Invalid --tls-version flag: %r' % tls_version) + + if disable_tls_compression: + raise ValueError( + '--disable-tls-compression is not available for ssl ' + 'module') + + self._tls_socket = ssl.wrap_socket(raw_socket, ssl_version=version) + + # Print cipher in use. Handshake is done on wrap_socket call. + self._logger.info("Cipher: %s", self._tls_socket.cipher()) + elif tls_module == _TLS_BY_PYOPENSSL: + if tls_version == _TLS_VERSION_SSL23: + version = OpenSSL.SSL.SSLv23_METHOD + elif tls_version == _TLS_VERSION_SSL3: + version = OpenSSL.SSL.SSLv3_METHOD + elif tls_version == _TLS_VERSION_TLS1: + version = OpenSSL.SSL.TLSv1_METHOD + else: + raise ValueError( + 'Invalid --tls-version flag: %r' % tls_version) + + context = OpenSSL.SSL.Context(version) + + if disable_tls_compression: + # OP_NO_COMPRESSION is not defined in OpenSSL module. + context.set_options(0x00020000) + + self._tls_socket = OpenSSL.SSL.Connection(context, raw_socket) + # Client mode. + self._tls_socket.set_connect_state() + self._tls_socket.setblocking(True) + + # Do handshake now (not necessary). + self._tls_socket.do_handshake() + else: + raise ValueError('No TLS support module is available') + + def send(self, data): + return self._tls_socket.write(data) + + def sendall(self, data): + return self._tls_socket.sendall(data) + + def recv(self, size=-1): + return self._tls_socket.read(size) + + def close(self): + return self._tls_socket.close() + + def getpeername(self): + return self._tls_socket.getpeername() + + +class ClientHandshakeBase(object): + """A base class for WebSocket opening handshake processors for each + protocol version. + """ + + def __init__(self): + self._logger = util.get_class_logger(self) + + def _read_fields(self): + # 4.1 32. let /fields/ be a list of name-value pairs, initially empty. + fields = {} + while True: # "Field" + # 4.1 33. let /name/ and /value/ be empty byte arrays + name = '' + value = '' + # 4.1 34. read /name/ + name = self._read_name() + if name is None: + break + # 4.1 35. read spaces + # TODO(tyoshino): Skip only one space as described in the spec. + ch = self._skip_spaces() + # 4.1 36. read /value/ + value = self._read_value(ch) + # 4.1 37. read a byte from the server + ch = _receive_bytes(self._socket, 1) + if ch != '\n': # 0x0A + raise ClientHandshakeError( + 'Expected LF but found %r while reading value %r for ' + 'header %r' % (ch, value, name)) + self._logger.debug('Received %r header', name) + # 4.1 38. append an entry to the /fields/ list that has the name + # given by the string obtained by interpreting the /name/ byte + # array as a UTF-8 stream and the value given by the string + # obtained by interpreting the /value/ byte array as a UTF-8 byte + # stream. + fields.setdefault(name, []).append(value) + # 4.1 39. return to the "Field" step above + return fields + + def _read_name(self): + # 4.1 33. let /name/ be empty byte arrays + name = '' + while True: + # 4.1 34. read a byte from the server + ch = _receive_bytes(self._socket, 1) + if ch == '\r': # 0x0D + return None + elif ch == '\n': # 0x0A + raise ClientHandshakeError( + 'Unexpected LF when reading header name %r' % name) + elif ch == ':': # 0x3A + return name + elif ch >= 'A' and ch <= 'Z': # Range 0x31 to 0x5A + ch = chr(ord(ch) + 0x20) + name += ch + else: + name += ch + + def _skip_spaces(self): + # 4.1 35. read a byte from the server + while True: + ch = _receive_bytes(self._socket, 1) + if ch == ' ': # 0x20 + continue + return ch + + def _read_value(self, ch): + # 4.1 33. let /value/ be empty byte arrays + value = '' + # 4.1 36. read a byte from server. + while True: + if ch == '\r': # 0x0D + return value + elif ch == '\n': # 0x0A + raise ClientHandshakeError( + 'Unexpected LF when reading header value %r' % value) + else: + value += ch + ch = _receive_bytes(self._socket, 1) + + +def _get_permessage_deflate_framer(extension_response): + """Validate the response and return a framer object using the parameters in + the response. This method doesn't accept the server_.* parameters. + """ + + client_max_window_bits = None + client_no_context_takeover = None + + client_max_window_bits_name = ( + PerMessageDeflateExtensionProcessor. + _CLIENT_MAX_WINDOW_BITS_PARAM) + client_no_context_takeover_name = ( + PerMessageDeflateExtensionProcessor. + _CLIENT_NO_CONTEXT_TAKEOVER_PARAM) + + # We didn't send any server_.* parameter. + # Handle those parameters as invalid if found in the response. + + for param_name, param_value in extension_response.get_parameters(): + if param_name == client_max_window_bits_name: + if client_max_window_bits is not None: + raise ClientHandshakeError( + 'Multiple %s found' % client_max_window_bits_name) + + parsed_value = _parse_window_bits(param_value) + if parsed_value is None: + raise ClientHandshakeError( + 'Bad %s: %r' % + (client_max_window_bits_name, param_value)) + client_max_window_bits = parsed_value + elif param_name == client_no_context_takeover_name: + if client_no_context_takeover is not None: + raise ClientHandshakeError( + 'Multiple %s found' % client_no_context_takeover_name) + + if param_value is not None: + raise ClientHandshakeError( + 'Bad %s: Has value %r' % + (client_no_context_takeover_name, param_value)) + client_no_context_takeover = True + + if client_no_context_takeover is None: + client_no_context_takeover = False + + return _PerMessageDeflateFramer(client_max_window_bits, + client_no_context_takeover) + + +class ClientHandshakeProcessor(ClientHandshakeBase): + """WebSocket opening handshake processor for + draft-ietf-hybi-thewebsocketprotocol-06 and later. + """ + + def __init__(self, socket, options): + super(ClientHandshakeProcessor, self).__init__() + + self._socket = socket + self._options = options + + self._logger = util.get_class_logger(self) + + def handshake(self): + """Performs opening handshake on the specified socket. + + Raises: + ClientHandshakeError: handshake failed. + """ + + request_line = _build_method_line(self._options.resource) + self._logger.debug('Client\'s opening handshake Request-Line: %r', + request_line) + self._socket.sendall(request_line) + + fields = [] + fields.append(_format_host_header( + self._options.server_host, + self._options.server_port, + self._options.use_tls)) + fields.append(_UPGRADE_HEADER) + fields.append(_CONNECTION_HEADER) + if self._options.origin is not None: + if self._options.protocol_version == _PROTOCOL_VERSION_HYBI08: + fields.append(_origin_header( + common.SEC_WEBSOCKET_ORIGIN_HEADER, + self._options.origin)) + else: + fields.append(_origin_header(common.ORIGIN_HEADER, + self._options.origin)) + + original_key = os.urandom(16) + self._key = base64.b64encode(original_key) + self._logger.debug( + '%s: %r (%s)', + common.SEC_WEBSOCKET_KEY_HEADER, + self._key, + util.hexify(original_key)) + fields.append( + '%s: %s\r\n' % (common.SEC_WEBSOCKET_KEY_HEADER, self._key)) + + if self._options.version_header > 0: + fields.append('%s: %d\r\n' % (common.SEC_WEBSOCKET_VERSION_HEADER, + self._options.version_header)) + elif self._options.protocol_version == _PROTOCOL_VERSION_HYBI08: + fields.append('%s: %d\r\n' % (common.SEC_WEBSOCKET_VERSION_HEADER, + common.VERSION_HYBI08)) + else: + fields.append('%s: %d\r\n' % (common.SEC_WEBSOCKET_VERSION_HEADER, + common.VERSION_HYBI_LATEST)) + + extensions_to_request = [] + + if self._options.deflate_frame: + extensions_to_request.append( + common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)) + + if self._options.use_permessage_deflate: + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + # Accept the client_max_window_bits extension parameter by default. + extension.add_parameter( + PerMessageDeflateExtensionProcessor. + _CLIENT_MAX_WINDOW_BITS_PARAM, + None) + extensions_to_request.append(extension) + + if len(extensions_to_request) != 0: + fields.append( + '%s: %s\r\n' % + (common.SEC_WEBSOCKET_EXTENSIONS_HEADER, + common.format_extensions(extensions_to_request))) + + for field in fields: + self._socket.sendall(field) + + self._socket.sendall('\r\n') + + self._logger.debug('Sent client\'s opening handshake headers: %r', + fields) + self._logger.debug('Start reading Status-Line') + + status_line = '' + while True: + ch = _receive_bytes(self._socket, 1) + status_line += ch + if ch == '\n': + break + + m = re.match('HTTP/\\d+\.\\d+ (\\d\\d\\d) .*\r\n', status_line) + if m is None: + raise ClientHandshakeError( + 'Wrong status line format: %r' % status_line) + status_code = m.group(1) + if status_code != '101': + self._logger.debug('Unexpected status code %s with following ' + 'headers: %r', status_code, self._read_fields()) + raise ClientHandshakeError( + 'Expected HTTP status code 101 but found %r' % status_code) + + self._logger.debug('Received valid Status-Line') + self._logger.debug('Start reading headers until we see an empty line') + + fields = self._read_fields() + + ch = _receive_bytes(self._socket, 1) + if ch != '\n': # 0x0A + raise ClientHandshakeError( + 'Expected LF but found %r while reading value %r for header ' + 'name %r' % (ch, value, name)) + + self._logger.debug('Received an empty line') + self._logger.debug('Server\'s opening handshake headers: %r', fields) + + _validate_mandatory_header( + fields, + common.UPGRADE_HEADER, + common.WEBSOCKET_UPGRADE_TYPE, + False) + + _validate_mandatory_header( + fields, + common.CONNECTION_HEADER, + common.UPGRADE_CONNECTION_TYPE, + False) + + accept = _get_mandatory_header( + fields, common.SEC_WEBSOCKET_ACCEPT_HEADER) + + # Validate + try: + binary_accept = base64.b64decode(accept) + except TypeError, e: + raise HandshakeError( + 'Illegal value for header %s: %r' % + (common.SEC_WEBSOCKET_ACCEPT_HEADER, accept)) + + if len(binary_accept) != 20: + raise ClientHandshakeError( + 'Decoded value of %s is not 20-byte long' % + common.SEC_WEBSOCKET_ACCEPT_HEADER) + + self._logger.debug( + 'Response for challenge : %r (%s)', + accept, util.hexify(binary_accept)) + + binary_expected_accept = util.sha1_hash( + self._key + common.WEBSOCKET_ACCEPT_UUID).digest() + expected_accept = base64.b64encode(binary_expected_accept) + + self._logger.debug( + 'Expected response for challenge: %r (%s)', + expected_accept, util.hexify(binary_expected_accept)) + + if accept != expected_accept: + raise ClientHandshakeError( + 'Invalid %s header: %r (expected: %s)' % + (common.SEC_WEBSOCKET_ACCEPT_HEADER, accept, expected_accept)) + + deflate_frame_accepted = False + permessage_deflate_accepted = False + + extensions_header = fields.get( + common.SEC_WEBSOCKET_EXTENSIONS_HEADER.lower()) + accepted_extensions = [] + if extensions_header is not None and len(extensions_header) != 0: + accepted_extensions = common.parse_extensions(extensions_header[0]) + + # TODO(bashi): Support the new style perframe compression extension. + for extension in accepted_extensions: + extension_name = extension.name() + if (extension_name == common.DEFLATE_FRAME_EXTENSION and + self._options.deflate_frame): + deflate_frame_accepted = True + processor = DeflateFrameExtensionProcessor(extension) + unused_extension_response = processor.get_extension_response() + self._options.deflate_frame = processor + continue + elif (extension_name == common.PERMESSAGE_DEFLATE_EXTENSION and + self._options.use_permessage_deflate): + permessage_deflate_accepted = True + + framer = _get_permessage_deflate_framer(extension) + framer.set_compress_outgoing_enabled(True) + self._options.use_permessage_deflate = framer + continue + + raise ClientHandshakeError( + 'Unexpected extension %r' % extension_name) + + if (self._options.deflate_frame and not deflate_frame_accepted): + raise ClientHandshakeError( + 'Requested %s, but the server rejected it' % + common.DEFLATE_FRAME_EXTENSION) + + if (self._options.use_permessage_deflate and + not permessage_deflate_accepted): + raise ClientHandshakeError( + 'Requested %s, but the server rejected it' % + common.PERMESSAGE_DEFLATE_EXTENSION) + + # TODO(tyoshino): Handle Sec-WebSocket-Protocol + # TODO(tyoshino): Handle Cookie, etc. + + +class ClientHandshakeProcessorHybi00(ClientHandshakeBase): + """WebSocket opening handshake processor for + draft-ietf-hybi-thewebsocketprotocol-00 (equivalent to + draft-hixie-thewebsocketprotocol-76). + """ + + def __init__(self, socket, options): + super(ClientHandshakeProcessorHybi00, self).__init__() + + self._socket = socket + self._options = options + + self._logger = util.get_class_logger(self) + + if (self._options.deflate_frame or + self._options.use_permessage_deflate): + logging.critical('HyBi 00 doesn\'t support extensions.') + sys.exit(1) + + def handshake(self): + """Performs opening handshake on the specified socket. + + Raises: + ClientHandshakeError: handshake failed. + """ + + # 4.1 5. send request line. + self._socket.sendall(_build_method_line(self._options.resource)) + # 4.1 6. Let /fields/ be an empty list of strings. + fields = [] + # 4.1 7. Add the string "Upgrade: WebSocket" to /fields/. + fields.append(_UPGRADE_HEADER_HIXIE75) + # 4.1 8. Add the string "Connection: Upgrade" to /fields/. + fields.append(_CONNECTION_HEADER) + # 4.1 9-12. Add Host: field to /fields/. + fields.append(_format_host_header( + self._options.server_host, + self._options.server_port, + self._options.use_tls)) + # 4.1 13. Add Origin: field to /fields/. + if not self._options.origin: + raise ClientHandshakeError( + 'Specify the origin of the connection by --origin flag') + fields.append(_origin_header(common.ORIGIN_HEADER, + self._options.origin)) + # TODO: 4.1 14 Add Sec-WebSocket-Protocol: field to /fields/. + # TODO: 4.1 15 Add cookie headers to /fields/. + + # 4.1 16-23. Add Sec-WebSocket-Key<n> to /fields/. + self._number1, key1 = self._generate_sec_websocket_key() + self._logger.debug('Number1: %d', self._number1) + fields.append('%s: %s\r\n' % (common.SEC_WEBSOCKET_KEY1_HEADER, key1)) + self._number2, key2 = self._generate_sec_websocket_key() + self._logger.debug('Number2: %d', self._number2) + fields.append('%s: %s\r\n' % (common.SEC_WEBSOCKET_KEY2_HEADER, key2)) + + fields.append('%s: 0\r\n' % common.SEC_WEBSOCKET_DRAFT_HEADER) + + # 4.1 24. For each string in /fields/, in a random order: send the + # string, encoded as UTF-8, followed by a UTF-8 encoded U+000D CARRIAGE + # RETURN U+000A LINE FEED character pair (CRLF). + random.shuffle(fields) + for field in fields: + self._socket.sendall(field) + # 4.1 25. send a UTF-8-encoded U+000D CARRIAGE RETURN U+000A LINE FEED + # character pair (CRLF). + self._socket.sendall('\r\n') + # 4.1 26. let /key3/ be a string consisting of eight random bytes (or + # equivalently, a random 64 bit integer encoded in a big-endian order). + self._key3 = self._generate_key3() + # 4.1 27. send /key3/ to the server. + self._socket.sendall(self._key3) + self._logger.debug( + 'Key3: %r (%s)', self._key3, util.hexify(self._key3)) + + self._logger.info('Sent handshake') + + # 4.1 28. Read bytes from the server until either the connection + # closes, or a 0x0A byte is read. let /field/ be these bytes, including + # the 0x0A bytes. + field = '' + while True: + ch = _receive_bytes(self._socket, 1) + field += ch + if ch == '\n': + break + # if /field/ is not at least seven bytes long, or if the last + # two bytes aren't 0x0D and 0x0A respectively, or if it does not + # contain at least two 0x20 bytes, then fail the WebSocket connection + # and abort these steps. + if len(field) < 7 or not field.endswith('\r\n'): + raise ClientHandshakeError('Wrong status line: %r' % field) + m = re.match('[^ ]* ([^ ]*) .*', field) + if m is None: + raise ClientHandshakeError( + 'No HTTP status code found in status line: %r' % field) + # 4.1 29. let /code/ be the substring of /field/ that starts from the + # byte after the first 0x20 byte, and ends with the byte before the + # second 0x20 byte. + code = m.group(1) + # 4.1 30. if /code/ is not three bytes long, or if any of the bytes in + # /code/ are not in the range 0x30 to 0x90, then fail the WebSocket + # connection and abort these steps. + if not re.match('[0-9][0-9][0-9]', code): + raise ClientHandshakeError( + 'HTTP status code %r is not three digit in status line: %r' % + (code, field)) + # 4.1 31. if /code/, interpreted as UTF-8, is "101", then move to the + # next step. + if code != '101': + raise ClientHandshakeError( + 'Expected HTTP status code 101 but found %r in status line: ' + '%r' % (code, field)) + # 4.1 32-39. read fields into /fields/ + fields = self._read_fields() + # 4.1 40. _Fields processing_ + # read a byte from server + ch = _receive_bytes(self._socket, 1) + if ch != '\n': # 0x0A + raise ClientHandshakeError('Expected LF but found %r' % ch) + # 4.1 41. check /fields/ + # TODO(ukai): protocol + # if the entry's name is "upgrade" + # if the value is not exactly equal to the string "WebSocket", + # then fail the WebSocket connection and abort these steps. + _validate_mandatory_header( + fields, + common.UPGRADE_HEADER, + common.WEBSOCKET_UPGRADE_TYPE_HIXIE75, + True) + # if the entry's name is "connection" + # if the value, converted to ASCII lowercase, is not exactly equal + # to the string "upgrade", then fail the WebSocket connection and + # abort these steps. + _validate_mandatory_header( + fields, + common.CONNECTION_HEADER, + common.UPGRADE_CONNECTION_TYPE, + False) + + origin = _get_mandatory_header( + fields, common.SEC_WEBSOCKET_ORIGIN_HEADER) + + location = _get_mandatory_header( + fields, common.SEC_WEBSOCKET_LOCATION_HEADER) + + # TODO(ukai): check origin, location, cookie, .. + + # 4.1 42. let /challenge/ be the concatenation of /number_1/, + # expressed as a big endian 32 bit integer, /number_2/, expressed + # as big endian 32 bit integer, and the eight bytes of /key_3/ in the + # order they were sent on the wire. + challenge = struct.pack('!I', self._number1) + challenge += struct.pack('!I', self._number2) + challenge += self._key3 + + self._logger.debug( + 'Challenge: %r (%s)', challenge, util.hexify(challenge)) + + # 4.1 43. let /expected/ be the MD5 fingerprint of /challenge/ as a + # big-endian 128 bit string. + expected = util.md5_hash(challenge).digest() + self._logger.debug( + 'Expected challenge response: %r (%s)', + expected, util.hexify(expected)) + + # 4.1 44. read sixteen bytes from the server. + # let /reply/ be those bytes. + reply = _receive_bytes(self._socket, 16) + self._logger.debug( + 'Actual challenge response: %r (%s)', reply, util.hexify(reply)) + + # 4.1 45. if /reply/ does not exactly equal /expected/, then fail + # the WebSocket connection and abort these steps. + if expected != reply: + raise ClientHandshakeError( + 'Bad challenge response: %r (expected) != %r (actual)' % + (expected, reply)) + # 4.1 46. The *WebSocket connection is established*. + + def _generate_sec_websocket_key(self): + # 4.1 16. let /spaces_n/ be a random integer from 1 to 12 inclusive. + spaces = random.randint(1, 12) + # 4.1 17. let /max_n/ be the largest integer not greater than + # 4,294,967,295 divided by /spaces_n/. + maxnum = 4294967295 / spaces + # 4.1 18. let /number_n/ be a random integer from 0 to /max_n/ + # inclusive. + number = random.randint(0, maxnum) + # 4.1 19. let /product_n/ be the result of multiplying /number_n/ and + # /spaces_n/ together. + product = number * spaces + # 4.1 20. let /key_n/ be a string consisting of /product_n/, expressed + # in base ten using the numerals in the range U+0030 DIGIT ZERO (0) to + # U+0039 DIGIT NINE (9). + key = str(product) + # 4.1 21. insert between one and twelve random characters from the + # range U+0021 to U+002F and U+003A to U+007E into /key_n/ at random + # positions. + available_chars = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1) + n = random.randint(1, 12) + for _ in xrange(n): + ch = random.choice(available_chars) + pos = random.randint(0, len(key)) + key = key[0:pos] + chr(ch) + key[pos:] + # 4.1 22. insert /spaces_n/ U+0020 SPACE characters into /key_n/ at + # random positions other than start or end of the string. + for _ in xrange(spaces): + pos = random.randint(1, len(key) - 1) + key = key[0:pos] + ' ' + key[pos:] + return number, key + + def _generate_key3(self): + # 4.1 26. let /key3/ be a string consisting of eight random bytes (or + # equivalently, a random 64 bit integer encoded in a big-endian order). + return ''.join([chr(random.randint(0, 255)) for _ in xrange(8)]) + + +class ClientConnection(object): + """A wrapper for socket object to provide the mp_conn interface. + mod_pywebsocket library is designed to be working on Apache mod_python's + mp_conn object. + """ + + def __init__(self, socket): + self._socket = socket + + def write(self, data): + self._socket.sendall(data) + + def read(self, n): + return self._socket.recv(n) + + def get_remote_addr(self): + return self._socket.getpeername() + remote_addr = property(get_remote_addr) + + +class ClientRequest(object): + """A wrapper class just to make it able to pass a socket object to + functions that expect a mp_request object. + """ + + def __init__(self, socket): + self._logger = util.get_class_logger(self) + + self._socket = socket + self.connection = ClientConnection(socket) + + +def _import_ssl(): + global ssl + try: + import ssl + return True + except ImportError: + return False + + +def _import_pyopenssl(): + global OpenSSL + try: + import OpenSSL.SSL + return True + except ImportError: + return False + + +class EchoClient(object): + """WebSocket echo client.""" + + def __init__(self, options): + self._options = options + self._socket = None + + self._logger = util.get_class_logger(self) + + def run(self): + """Run the client. + + Shake hands and then repeat sending message and receiving its echo. + """ + + self._socket = socket.socket() + self._socket.settimeout(self._options.socket_timeout) + try: + self._socket.connect((self._options.server_host, + self._options.server_port)) + if self._options.use_tls: + self._socket = _TLSSocket( + self._socket, + self._options.tls_module, + self._options.tls_version, + self._options.disable_tls_compression) + + version = self._options.protocol_version + + if (version == _PROTOCOL_VERSION_HYBI08 or + version == _PROTOCOL_VERSION_HYBI13): + self._handshake = ClientHandshakeProcessor( + self._socket, self._options) + elif version == _PROTOCOL_VERSION_HYBI00: + self._handshake = ClientHandshakeProcessorHybi00( + self._socket, self._options) + else: + raise ValueError( + 'Invalid --protocol-version flag: %r' % version) + + self._handshake.handshake() + + self._logger.info('Connection established') + + request = ClientRequest(self._socket) + + version_map = { + _PROTOCOL_VERSION_HYBI08: common.VERSION_HYBI08, + _PROTOCOL_VERSION_HYBI13: common.VERSION_HYBI13, + _PROTOCOL_VERSION_HYBI00: common.VERSION_HYBI00} + request.ws_version = version_map[version] + + if (version == _PROTOCOL_VERSION_HYBI08 or + version == _PROTOCOL_VERSION_HYBI13): + stream_option = StreamOptions() + stream_option.mask_send = True + stream_option.unmask_receive = False + + if self._options.deflate_frame is not False: + processor = self._options.deflate_frame + processor.setup_stream_options(stream_option) + + if self._options.use_permessage_deflate is not False: + framer = self._options.use_permessage_deflate + framer.setup_stream_options(stream_option) + + self._stream = Stream(request, stream_option) + elif version == _PROTOCOL_VERSION_HYBI00: + self._stream = StreamHixie75(request, True) + + for line in self._options.message.split(','): + self._stream.send_message(line) + if self._options.verbose: + print 'Send: %s' % line + try: + received = self._stream.receive_message() + + if self._options.verbose: + print 'Recv: %s' % received + except Exception, e: + if self._options.verbose: + print 'Error: %s' % e + raise + + self._do_closing_handshake() + finally: + self._socket.close() + + def _do_closing_handshake(self): + """Perform closing handshake using the specified closing frame.""" + + if self._options.message.split(',')[-1] == _GOODBYE_MESSAGE: + # requested server initiated closing handshake, so + # expecting closing handshake message from server. + self._logger.info('Wait for server-initiated closing handshake') + message = self._stream.receive_message() + if message is None: + print 'Recv close' + print 'Send ack' + self._logger.info( + 'Received closing handshake and sent ack') + return + print 'Send close' + self._stream.close_connection() + self._logger.info('Sent closing handshake') + print 'Recv ack' + self._logger.info('Received ack') + + +def main(): + sys.stdout = codecs.getwriter('utf-8')(sys.stdout) + + parser = OptionParser() + # We accept --command_line_flag style flags which is the same as Google + # gflags in addition to common --command-line-flag style flags. + parser.add_option('-s', '--server-host', '--server_host', + dest='server_host', type='string', + default='localhost', help='server host') + parser.add_option('-p', '--server-port', '--server_port', + dest='server_port', type='int', + default=_UNDEFINED_PORT, help='server port') + parser.add_option('-o', '--origin', dest='origin', type='string', + default=None, help='origin') + parser.add_option('-r', '--resource', dest='resource', type='string', + default='/echo', help='resource path') + parser.add_option('-m', '--message', dest='message', type='string', + help=('comma-separated messages to send. ' + '%s will force close the connection from server.' % + _GOODBYE_MESSAGE)) + parser.add_option('-q', '--quiet', dest='verbose', action='store_false', + default=True, help='suppress messages') + parser.add_option('-t', '--tls', dest='use_tls', action='store_true', + default=False, help='use TLS (wss://). By default, ' + 'it looks for ssl and pyOpenSSL module and uses found ' + 'one. Use --tls-module option to specify which module ' + 'to use') + parser.add_option('--tls-module', '--tls_module', dest='tls_module', + type='choice', + choices=[_TLS_BY_STANDARD_MODULE, _TLS_BY_PYOPENSSL], + help='Use ssl module if "%s" is specified. ' + 'Use pyOpenSSL module if "%s" is specified' % + (_TLS_BY_STANDARD_MODULE, _TLS_BY_PYOPENSSL)) + parser.add_option('--tls-version', '--tls_version', + dest='tls_version', + type='string', default=_TLS_VERSION_SSL23, + help='TLS/SSL version to use. One of \'' + + _TLS_VERSION_SSL23 + '\' (SSL version 2 or 3), \'' + + _TLS_VERSION_SSL3 + '\' (SSL version 3), \'' + + _TLS_VERSION_TLS1 + '\' (TLS version 1)') + parser.add_option('--disable-tls-compression', '--disable_tls_compression', + dest='disable_tls_compression', + action='store_true', default=False, + help='Disable TLS compression. Available only when ' + 'pyOpenSSL module is used.') + parser.add_option('-k', '--socket-timeout', '--socket_timeout', + dest='socket_timeout', type='int', default=_TIMEOUT_SEC, + help='Timeout(sec) for sockets') + parser.add_option('--draft75', dest='draft75', + action='store_true', default=False, + help='Obsolete option. Don\'t use this.') + parser.add_option('--protocol-version', '--protocol_version', + dest='protocol_version', + type='string', default=_PROTOCOL_VERSION_HYBI13, + help='WebSocket protocol version to use. One of \'' + + _PROTOCOL_VERSION_HYBI13 + '\', \'' + + _PROTOCOL_VERSION_HYBI08 + '\', \'' + + _PROTOCOL_VERSION_HYBI00 + '\'') + parser.add_option('--version-header', '--version_header', + dest='version_header', + type='int', default=-1, + help='Specify Sec-WebSocket-Version header value') + parser.add_option('--deflate-frame', '--deflate_frame', + dest='deflate_frame', + action='store_true', default=False, + help='Use the deflate-frame extension.') + parser.add_option('--use-permessage-deflate', '--use_permessage_deflate', + dest='use_permessage_deflate', + action='store_true', default=False, + help='Use the permessage-deflate extension.') + parser.add_option('--log-level', '--log_level', type='choice', + dest='log_level', default='warn', + choices=['debug', 'info', 'warn', 'error', 'critical'], + help='Log level.') + + (options, unused_args) = parser.parse_args() + + logging.basicConfig(level=logging.getLevelName(options.log_level.upper())) + + if options.draft75: + logging.critical('--draft75 option is obsolete.') + sys.exit(1) + + if options.protocol_version == _PROTOCOL_VERSION_HIXIE75: + logging.critical( + 'Value %s is obsolete for --protocol_version options' % + _PROTOCOL_VERSION_HIXIE75) + sys.exit(1) + + if options.use_tls: + if options.tls_module is None: + if _import_ssl(): + options.tls_module = _TLS_BY_STANDARD_MODULE + logging.debug('Using ssl module') + elif _import_pyopenssl(): + options.tls_module = _TLS_BY_PYOPENSSL + logging.debug('Using pyOpenSSL module') + else: + logging.critical( + 'TLS support requires ssl or pyOpenSSL module.') + sys.exit(1) + elif options.tls_module == _TLS_BY_STANDARD_MODULE: + if not _import_ssl(): + logging.critical('ssl module is not available') + sys.exit(1) + elif options.tls_module == _TLS_BY_PYOPENSSL: + if not _import_pyopenssl(): + logging.critical('pyOpenSSL module is not available') + sys.exit(1) + else: + logging.critical('Invalid --tls-module option: %r', + options.tls_module) + sys.exit(1) + + if (options.disable_tls_compression and + options.tls_module != _TLS_BY_PYOPENSSL): + logging.critical('You can disable TLS compression only when ' + 'pyOpenSSL module is used.') + sys.exit(1) + else: + if options.tls_module is not None: + logging.critical('Use --tls-module option only together with ' + '--use-tls option.') + sys.exit(1) + + if options.disable_tls_compression: + logging.critical('Use --disable-tls-compression only together ' + 'with --use-tls option.') + sys.exit(1) + + # Default port number depends on whether TLS is used. + if options.server_port == _UNDEFINED_PORT: + if options.use_tls: + options.server_port = common.DEFAULT_WEB_SOCKET_SECURE_PORT + else: + options.server_port = common.DEFAULT_WEB_SOCKET_PORT + + # optparse doesn't seem to handle non-ascii default values. + # Set default message here. + if not options.message: + options.message = u'Hello,\u65e5\u672c' # "Japan" in Japanese + + EchoClient(options).run() + + +if __name__ == '__main__': + main() + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/echo_noext_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/echo_noext_wsh.py new file mode 100644 index 000000000..1df515122 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/echo_noext_wsh.py @@ -0,0 +1,61 @@ +# Copyright 2013, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +_GOODBYE_MESSAGE = u'Goodbye' + + +def web_socket_do_extra_handshake(request): + """Received Sec-WebSocket-Extensions header value is parsed into + request.ws_requested_extensions. pywebsocket creates extension + processors using it before do_extra_handshake call and never looks at it + after the call. + + To reject requested extensions, clear the processor list. + """ + + request.ws_extension_processors = [] + + +def web_socket_transfer_data(request): + """Echo. Same as echo_wsh.py.""" + + while True: + line = request.ws_stream.receive_message() + if line is None: + return + if isinstance(line, unicode): + request.ws_stream.send_message(line, binary=False) + if line == _GOODBYE_MESSAGE: + return + else: + request.ws_stream.send_message(line, binary=True) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/echo_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/echo_wsh.py new file mode 100644 index 000000000..38646c32c --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/echo_wsh.py @@ -0,0 +1,54 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +_GOODBYE_MESSAGE = u'Goodbye' + + +def web_socket_do_extra_handshake(request): + # This example handler accepts any request. See origin_check_wsh.py for how + # to reject access from untrusted scripts based on origin value. + + pass # Always accept. + + +def web_socket_transfer_data(request): + while True: + line = request.ws_stream.receive_message() + if line is None: + return + if isinstance(line, unicode): + request.ws_stream.send_message(line, binary=False) + if line == _GOODBYE_MESSAGE: + return + else: + request.ws_stream.send_message(line, binary=True) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/eventsource.cgi b/testing/web-platform/tests/tools/pywebsocket/src/example/eventsource.cgi new file mode 100755 index 000000000..adddf237c --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/eventsource.cgi @@ -0,0 +1,54 @@ +#!/usr/bin/python + +# Copyright 2013, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""This CGI script generates text/event-stream type data stream for testing +the Server-Sent Events. + +It will only work correctly with HTTP servers that do not buffer the output of +CGI scripts. +""" + + +import sys +import time + +sys.stdout.write('Content-type: text/event-stream\r\n\r\n') + +id = 0 + +while True: + sys.stdout.write('data: Hello\r\nid: %d\r\n\r\n' % id) + sys.stdout.flush() + + id = id + 1 + + time.sleep(1) diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/eventsource.html b/testing/web-platform/tests/tools/pywebsocket/src/example/eventsource.html new file mode 100644 index 000000000..1598a8807 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/eventsource.html @@ -0,0 +1,74 @@ +<!-- +Copyright 2013, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--> + +<!-- +Simple example of the Server-Sent Events +http://dev.w3.org/html5/eventsource/ + +For comparison with the WebSocket Protocol & API. + +Run the pywebsocket with the --cgi_path parameter. +--> + +<html> +<head> +<title>Server-Sent Events Example</title> +<script> +var eventSource = null; + +function addToLog(data) { + logBox.value += data + '\n'; + logBox.scrollTop = 1000000; +} + +function init() { + logBox = document.getElementById('log'); + + eventSource = new EventSource('/eventsource.cgi'); + eventSource.onopen = function() { + addToLog('onopen (readyState = ' + eventSource.readyState + ')'); + } + eventSource.onmessage = function(event) { + addToLog(event.data); + } + eventSource.onerror = function(event) { + addToLog('onerror (readyState = ' + eventSource.readyState + ')'); + } +} +</script> +</head> +<body onload="init()"> +<textarea id="log" rows="10" cols="40" readonly></textarea> +<p style="font-size: small"> + Make sure that pywebsocket is run with --cgi_path parameter. +</p> +</body> +</html> diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/handler_map.txt b/testing/web-platform/tests/tools/pywebsocket/src/example/handler_map.txt new file mode 100644 index 000000000..21c4c09aa --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/handler_map.txt @@ -0,0 +1,11 @@ +# websocket handler map file, used by standalone.py -m option. +# A line starting with '#' is a comment line. +# Each line consists of 'alias_resource_path' and 'existing_resource_path' +# separated by spaces. +# Aliasing is processed from the top to the bottom of the line, and +# 'existing_resource_path' must exist before it is aliased. +# For example, +# / /echo +# means that a request to '/' will be handled by handlers for '/echo'. +/ /echo + diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/hsts_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/hsts_wsh.py new file mode 100644 index 000000000..e86194692 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/hsts_wsh.py @@ -0,0 +1,40 @@ +# Copyright 2013, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +def web_socket_do_extra_handshake(request): + request.extra_headers.append( + ('Strict-Transport-Security', 'max-age=86400')) + + +def web_socket_transfer_data(request): + request.ws_stream.send_message('Hello', binary=False) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/internal_error_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/internal_error_wsh.py new file mode 100644 index 000000000..fe581b54a --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/internal_error_wsh.py @@ -0,0 +1,42 @@ +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +from mod_pywebsocket import msgutil + + +def web_socket_do_extra_handshake(request): + pass + + +def web_socket_transfer_data(request): + raise msgutil.BadOperationException('Intentional') + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/origin_check_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/example/origin_check_wsh.py new file mode 100644 index 000000000..e05767ab9 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/origin_check_wsh.py @@ -0,0 +1,44 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# This example is derived from test/testdata/handlers/origin_check_wsh.py. + + +def web_socket_do_extra_handshake(request): + if request.ws_origin == 'http://example.com': + return + raise ValueError('Unacceptable origin: %r' % request.ws_origin) + + +def web_socket_transfer_data(request): + request.connection.write('origin_check_wsh.py is called for %s, %s' % + (request.ws_resource, request.ws_protocol)) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/pywebsocket.conf b/testing/web-platform/tests/tools/pywebsocket/src/example/pywebsocket.conf new file mode 100644 index 000000000..335d130a5 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/pywebsocket.conf @@ -0,0 +1,42 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# +# Sample configuration file for apache2 +# +LogLevel debug +<IfModule python_module> + PythonPath "sys.path+['/mod_pywebsocket']" + PythonOption mod_pywebsocket.handler_root /var/www + PythonOption mod_pywebsocket.handler_scan /var/www/ws + #PythonOption mod_pywebsocket.allow_draft75 On + <Location /ws> + PythonHeaderParserHandler mod_pywebsocket.headerparserhandler + </Location> +</IfModule> diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/special_headers.cgi b/testing/web-platform/tests/tools/pywebsocket/src/example/special_headers.cgi new file mode 100755 index 000000000..ea5080f1f --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/special_headers.cgi @@ -0,0 +1,28 @@ +#!/usr/bin/python + +# Copyright 2014 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the COPYING file or at +# https://developers.google.com/open-source/licenses/bsd + +"""CGI script sample for testing effect of HTTP headers on the origin page. + +Note that CGI scripts don't work on the standalone pywebsocket running in TLS +mode. +""" + + +print """Content-type: text/html +Content-Security-Policy: connect-src self + +<html> +<head> +<title></title> +</head> +<body> +<script> +var socket = new WebSocket("ws://example.com"); +</script> +</body> +</html>""" diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/util.js b/testing/web-platform/tests/tools/pywebsocket/src/example/util.js new file mode 100644 index 000000000..a1cad4975 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/util.js @@ -0,0 +1,177 @@ +// Copyright 2013, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +// Utilities for example applications (for both main and worker thread). + +var results = {}; + +function getTimeStamp() { + return Date.now(); +} + +function formatResultInKiB(size, timePerMessageInMs, stddevTimePerMessageInMs, + speed, printSize) { + if (printSize) { + return (size / 1024) + + '\t' + timePerMessageInMs.toFixed(3) + + (stddevTimePerMessageInMs == -1 ? + '' : + '\t' + stddevTimePerMessageInMs.toFixed(3)) + + '\t' + speed.toFixed(3); + } else { + return speed.toString(); + } +} + +function clearAverageData() { + results = {}; +} + +function reportAverageData(config) { + config.addToSummary( + 'Size[KiB]\tAverage time[ms]\tStddev time[ms]\tSpeed[KB/s]'); + for (var size in results) { + var averageTimePerMessageInMs = results[size].sum_t / results[size].n; + var speed = calculateSpeedInKB(size, averageTimePerMessageInMs); + // Calculate sample standard deviation + var stddevTimePerMessageInMs = Math.sqrt( + (results[size].sum_t2 / results[size].n - + averageTimePerMessageInMs * averageTimePerMessageInMs) * + results[size].n / + (results[size].n - 1)); + config.addToSummary(formatResultInKiB( + size, averageTimePerMessageInMs, stddevTimePerMessageInMs, speed, + true)); + } +} + +function calculateSpeedInKB(size, timeSpentInMs) { + return Math.round(size / timeSpentInMs * 1000) / 1000; +} + +function calculateAndLogResult(config, size, startTimeInMs, totalSize) { + var timeSpentInMs = getTimeStamp() - startTimeInMs; + var speed = calculateSpeedInKB(totalSize, timeSpentInMs); + var timePerMessageInMs = timeSpentInMs / (totalSize / size); + if (!results[size]) { + results[size] = {n: 0, sum_t: 0, sum_t2: 0}; + } + config.measureValue(timePerMessageInMs); + results[size].n ++; + results[size].sum_t += timePerMessageInMs; + results[size].sum_t2 += timePerMessageInMs * timePerMessageInMs; + config.addToLog(formatResultInKiB(size, timePerMessageInMs, -1, speed, + config.printSize)); +} + +function fillArrayBuffer(buffer, c) { + var i; + + var u32Content = c * 0x01010101; + + var u32Blocks = Math.floor(buffer.byteLength / 4); + var u32View = new Uint32Array(buffer, 0, u32Blocks); + // length attribute is slow on Chrome. Don't use it for loop condition. + for (i = 0; i < u32Blocks; ++i) { + u32View[i] = u32Content; + } + + // Fraction + var u8Blocks = buffer.byteLength - u32Blocks * 4; + var u8View = new Uint8Array(buffer, u32Blocks * 4, u8Blocks); + for (i = 0; i < u8Blocks; ++i) { + u8View[i] = c; + } +} + +function verifyArrayBuffer(buffer, expectedChar) { + var i; + + var expectedU32Value = expectedChar * 0x01010101; + + var u32Blocks = Math.floor(buffer.byteLength / 4); + var u32View = new Uint32Array(buffer, 0, u32Blocks); + for (i = 0; i < u32Blocks; ++i) { + if (u32View[i] != expectedU32Value) { + return false; + } + } + + var u8Blocks = buffer.byteLength - u32Blocks * 4; + var u8View = new Uint8Array(buffer, u32Blocks * 4, u8Blocks); + for (i = 0; i < u8Blocks; ++i) { + if (u8View[i] != expectedChar) { + return false; + } + } + + return true; +} + +function verifyBlob(config, blob, expectedChar, doneCallback) { + var reader = new FileReader(blob); + reader.onerror = function() { + config.addToLog('FileReader Error: ' + reader.error.message); + doneCallback(blob.size, false); + } + reader.onloadend = function() { + var result = verifyArrayBuffer(reader.result, expectedChar); + doneCallback(blob.size, result); + } + reader.readAsArrayBuffer(blob); +} + +function verifyAcknowledgement(config, message, size) { + if (typeof message != 'string') { + config.addToLog('Invalid ack type: ' + typeof message); + return false; + } + var parsedAck = parseInt(message); + if (isNaN(parsedAck)) { + config.addToLog('Invalid ack value: ' + message); + return false; + } + if (parsedAck != size) { + config.addToLog( + 'Expected ack for ' + size + 'B but received one for ' + parsedAck + + 'B'); + return false; + } + + return true; +} + +function cloneConfig(obj) { + var newObj = {}; + for (key in obj) { + newObj[key] = obj[key]; + } + return newObj; +} diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/util_main.js b/testing/web-platform/tests/tools/pywebsocket/src/example/util_main.js new file mode 100644 index 000000000..b03d1c2bd --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/util_main.js @@ -0,0 +1,63 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the COPYING file or at +// https://developers.google.com/open-source/licenses/bsd + +// Utilities for example applications (for the main thread only). + +var logBox = null; +var queuedLog = ''; + +var summaryBox = null; + +function queueLog(log) { + queuedLog += log + '\n'; +} + +function addToLog(log) { + logBox.value += queuedLog; + queuedLog = ''; + logBox.value += log + '\n'; + logBox.scrollTop = 1000000; +} + +function addToSummary(log) { + summaryBox.value += log + '\n'; + summaryBox.scrollTop = 1000000; +} + +// value: execution time in milliseconds. +// config.measureValue is intended to be used in Performance Tests. +// Do nothing here in non-PerformanceTest. +function measureValue(value) { +} + +function getIntFromInput(id) { + return parseInt(document.getElementById(id).value); +} + +function getStringFromRadioBox(name) { + var list = document.getElementById('benchmark_form')[name]; + for (var i = 0; i < list.length; ++i) + if (list.item(i).checked) + return list.item(i).value; + return undefined; +} +function getBoolFromCheckBox(id) { + return document.getElementById(id).checked; +} + +function getIntArrayFromInput(id) { + var strArray = document.getElementById(id).value.split(','); + return strArray.map(function(str) { return parseInt(str, 10); }); +} + +function onMessage(message) { + if (message.data.type === 'addToLog') + addToLog(message.data.data); + else if (message.data.type === 'addToSummary') + addToSummary(message.data.data); + else if (message.data.type === 'measureValue') + measureValue(message.data.data); +} diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/util_worker.js b/testing/web-platform/tests/tools/pywebsocket/src/example/util_worker.js new file mode 100644 index 000000000..b64f7829d --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/util_worker.js @@ -0,0 +1,19 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the COPYING file or at +// https://developers.google.com/open-source/licenses/bsd + +// Utilities for example applications (for the worker threads only). + +function workerAddToLog(text) { + postMessage({type: 'addToLog', data: text}); +} + +function workerAddToSummary(text) { + postMessage({type: 'addToSummary', data: text}); +} + +function workerMeasureValue(value) { + postMessage({type: 'measureValue', data: value}); +} diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_benchmark.html b/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_benchmark.html new file mode 100644 index 000000000..186229775 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_benchmark.html @@ -0,0 +1,222 @@ +<!-- +Copyright 2013, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--> + +<html> +<head> +<title>XMLHttpRequest benchmark</title> +<script src="util_main.js"></script> +<script src="util.js"></script> +<script src="xhr_benchmark.js"></script> +<script> +var addressBox = null; + +function getConfig() { + return { + prefixUrl: addressBox.value, + printSize: getBoolFromCheckBox('printsize'), + numXHRs: getIntFromInput('numXHRs'), + async: getBoolFromCheckBox('async'), + // Initial size of messages. + numIterations: getIntFromInput('numiterations'), + numWarmUpIterations: getIntFromInput('numwarmupiterations'), + startSize: getIntFromInput('startsize'), + // Stops benchmark when the size of message exceeds this threshold. + stopThreshold: getIntFromInput('stopthreshold'), + // If the size of each message is small, send/receive multiple messages + // until the sum of sizes reaches this threshold. + // minTotal: getIntFromInput('mintotal'), + // minTotal is not yet implemented on XHR benchmark + multipliers: getIntArrayFromInput('multipliers'), + verifyData: getBoolFromCheckBox('verifydata') + }; +} + +var worker = new Worker('xhr_benchmark.js'); +worker.onmessage = onMessage; + +function onSendBenchmark() { + var config = getConfig(); + config.dataType = getStringFromRadioBox('datatyperadio'); + + if (getBoolFromCheckBox('worker')) { + worker.postMessage({type: 'sendBenchmark', config: config}); + } else { + config.addToLog = addToLog; + config.addToSummary = addToSummary; + config.measureValue = measureValue; + sendBenchmark(config); + } +} + +function onReceiveBenchmark() { + var config = getConfig(); + config.dataType = getStringFromRadioBox('datatyperadio'); + + if (getBoolFromCheckBox('worker')) { + worker.postMessage({type: 'receiveBenchmark', config: config}); + } else { + config.addToLog = addToLog; + config.addToSummary = addToSummary; + config.measureValue = measureValue; + receiveBenchmark(config); + } +} + +function onBatchBenchmark() { + var config = getConfig(); + + if (getBoolFromCheckBox('worker')) { + worker.postMessage({type: 'batchBenchmark', config: config}); + } else { + config.addToLog = addToLog; + config.addToSummary = addToSummary; + config.measureValue = measureValue; + batchBenchmark(config); + } +} + +function onStop() { + var config = getConfig(); + + if (getBoolFromCheckBox('worker')) { + worker.postMessage({type: 'stop', config: config}); + } else { + config.addToLog = addToLog; + config.addToSummary = addToSummary; + config.measureValue = measureValue; + stop(config); + } +} + +function init() { + addressBox = document.getElementById('address'); + logBox = document.getElementById('log'); + + summaryBox = document.getElementById('summary'); + + // Special address of pywebsocket for XHR benchmark. + addressBox.value = '/073be001e10950692ccbf3a2ad21c245'; + + addToLog(window.navigator.userAgent.toLowerCase()); + addToSummary(window.navigator.userAgent.toLowerCase()); +} +</script> +</head> +<body onload="init()"> + +<form id="benchmark_form"> + url prefix <input type="text" id="address" size="40"> + <input type="button" value="send" onclick="onSendBenchmark()"> + <input type="button" value="receive" onclick="onReceiveBenchmark()"> + <input type="button" value="batch" onclick="onBatchBenchmark()"> + <input type="button" value="stop" onclick="onStop()"> + + <br/> + + <input type="checkbox" id="printsize" checked> + <label for="printsize">Print size and time per message</label> + <input type="checkbox" id="verifydata" checked> + <label for="verifydata">Verify data</label> + <input type="checkbox" id="worker"> + <label for="worker">Run on worker</label> + <input type="checkbox" id="async" checked> + <label for="async">Async</label><br> + (Receive && Non-Worker && Sync is not supported by spec) + + <br/> + + Parameters: + + <br/> + + <table> + <tr> + <td>Num XHRs</td> + <td><input type="text" id="numXHRs" value="1"></td> + </tr> + <tr> + <td>Number of iterations</td> + <td><input type="text" id="numiterations" value="1"></td> + </tr> + <tr> + <td>Number of warm-up iterations</td> + <td><input type="text" id="numwarmupiterations" value="0"></td> + </tr> + <tr> + <td>Start size</td> + <td><input type="text" id="startsize" value="10240"></td> + </tr> + <tr> + <td>Stop threshold</td> + <td><input type="text" id="stopthreshold" value="102400000"></td> + </tr> + <tr> + <td>Minimum total</td> + <td><input type="text" id="mintotal" value="102400000"></td> + </tr> + <tr> + <td>Multipliers</td> + <td><input type="text" id="multipliers" value="5, 2"></td> + </tr> + </table> + + Set data type + <input type="radio" + name="datatyperadio" + id="datatyperadiotext" + value="text" + checked><label for="datatyperadiotext">text</label> + <input type="radio" + name="datatyperadio" + id="datatyperadioblob" + value="blob" + ><label for="datatyperadioblob">blob</label> + <input type="radio" + name="datatyperadio" + id="datatyperadioarraybuffer" + value="arraybuffer" + ><label for="datatyperadioarraybuffer">arraybuffer</label> +</form> + +<div id="log_div"> + <textarea + id="log" rows="20" style="width: 100%" readonly></textarea> +</div> +<div id="summary_div"> + Summary + <textarea + id="summary" rows="20" style="width: 100%" readonly></textarea> +</div> + +Note: Effect of RTT and time spent for ArrayBuffer creation in receive benchmarks are not eliminated. + +</body> +</html> diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_benchmark.js b/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_benchmark.js new file mode 100644 index 000000000..233c7cb38 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_benchmark.js @@ -0,0 +1,389 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the COPYING file or at +// https://developers.google.com/open-source/licenses/bsd + +var isWorker = typeof importScripts !== "undefined"; + +if (isWorker) { + // Running on a worker + importScripts('util.js', 'util_worker.js'); +} + +// Namespace for holding globals. +var benchmark = {}; +benchmark.startTimeInMs = 0; + +var xhrs = []; + +var timerID = null; + +function destroyAllXHRs() { + for (var i = 0; i < xhrs.length; ++i) { + xhrs[i].onreadystatechange = null; + // Abort XHRs if they are not yet DONE state. + // Calling abort() here (i.e. in onreadystatechange handler) + // causes "NetworkError" messages in DevTools in sync mode, + // even if it is after transition to DONE state. + if (xhrs[i].readyState != XMLHttpRequest.DONE) + xhrs[i].abort(); + } + xhrs = []; + // gc() might be needed for Chrome/Blob +} + +function repeatString(str, count) { + var data = ''; + var expChunk = str; + var remain = count; + while (true) { + if (remain % 2) { + data += expChunk; + remain = (remain - 1) / 2; + } else { + remain /= 2; + } + + if (remain == 0) + break; + + expChunk = expChunk + expChunk; + } + return data; +} + +function sendBenchmarkStep(size, config) { + timerID = null; + + benchmark.startTimeInMs = null; + var totalSize = 0; + var totalReplied = 0; + + var onReadyStateChangeHandler = function () { + if (this.readyState != this.DONE) { + return; + } + + if (this.status != 200) { + config.addToLog('Failed (status=' + this.status + ')'); + destroyAllXHRs(); + return; + } + + if (config.verifyData && + !verifyAcknowledgement(config, this.response, size)) { + destroyAllXHRs(); + return; + } + + totalReplied += size; + + if (totalReplied < totalSize) { + return; + } + + if (benchmark.startTimeInMs == null) { + config.addToLog('startTimeInMs not set'); + destroyAllXHRs(); + return; + } + + calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize); + + destroyAllXHRs(); + + runNextTask(config); + }; + + for (var i = 0; i < config.numXHRs; ++i) { + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = onReadyStateChangeHandler; + xhrs.push(xhr); + } + + var dataArray = []; + + for (var i = 0; i < xhrs.length; ++i) { + var data = null; + if (config.dataType == 'arraybuffer' || + config.dataType == 'blob') { + data = new ArrayBuffer(size); + + fillArrayBuffer(data, 0x61); + + if (config.dataType == 'blob') { + data = new Blob([data]); + } + } else { + data = repeatString('a', size); + } + + dataArray.push(data); + } + + + benchmark.startTimeInMs = getTimeStamp(); + totalSize = size * xhrs.length; + + for (var i = 0; i < xhrs.length; ++i) { + var data = dataArray[i]; + var xhr = xhrs[i]; + xhr.open('POST', config.prefixUrl + '_send', config.async); + xhr.send(data); + } +} + +function receiveBenchmarkStep(size, config) { + timerID = null; + + benchmark.startTimeInMs = null; + var totalSize = 0; + var totalReplied = 0; + + var checkResultAndContinue = function (bytesReceived, verificationResult) { + if (!verificationResult) { + config.addToLog('Response verification failed'); + destroyAllXHRs(); + return; + } + + totalReplied += bytesReceived; + + if (totalReplied < totalSize) { + return; + } + + if (benchmark.startTimeInMs == null) { + config.addToLog('startTimeInMs not set'); + destroyAllXHRs(); + return; + } + + calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize); + + destroyAllXHRs(); + + runNextTask(config); + } + + var onReadyStateChangeHandler = function () { + if (this.readyState != this.DONE) { + return; + } + + if (this.status != 200) { + config.addToLog('Failed (status=' + this.status + ')'); + destroyAllXHRs(); + return; + } + + var bytesReceived = -1; + if (this.responseType == 'arraybuffer') { + bytesReceived = this.response.byteLength; + } else if (this.responseType == 'blob') { + bytesReceived = this.response.size; + } else { + bytesReceived = this.response.length; + } + if (bytesReceived != size) { + config.addToLog('Expected ' + size + + 'B but received ' + bytesReceived + 'B'); + destroyAllXHRs(); + return; + } + + if (this.responseType == 'arraybuffer') { + checkResultAndContinue(bytesReceived, + !config.verifyData || verifyArrayBuffer(this.response, 0x61)); + } else if (this.responseType == 'blob') { + if (config.verifyData) + verifyBlob(config, this.response, 0x61, checkResultAndContinue); + else + checkResultAndContinue(bytesReceived, true); + } else { + checkResultAndContinue( + bytesReceived, + !config.verifyData || + this.response == repeatString('a', this.response.length)); + } + }; + + for (var i = 0; i < config.numXHRs; ++i) { + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = onReadyStateChangeHandler; + xhrs.push(xhr); + } + + benchmark.startTimeInMs = getTimeStamp(); + totalSize = size * xhrs.length; + + for (var i = 0; i < xhrs.length; ++i) { + var xhr = xhrs[i]; + xhr.open('POST', config.prefixUrl + '_receive', config.async); + xhr.responseType = config.dataType; + xhr.send(size + ' none'); + } +} + + +function getConfigString(config) { + return '(' + config.dataType + + ', verifyData=' + config.verifyData + + ', ' + (isWorker ? 'Worker' : 'Main') + + ', ' + (config.async ? 'Async' : 'Sync') + + ', numXHRs=' + config.numXHRs + + ', numIterations=' + config.numIterations + + ', numWarmUpIterations=' + config.numWarmUpIterations + + ')'; +} + +function startBenchmark(config) { + clearTimeout(timerID); + destroyAllXHRs(); + + runNextTask(config); +} + +// TODO(hiroshige): the following code is the same as benchmark.html +// and some of them should be merged into e.g. util.js + +var tasks = []; + +function runNextTask(config) { + var task = tasks.shift(); + if (task == undefined) { + config.addToLog('Finished'); + destroyAllXHRs(); + return; + } + timerID = setTimeout(task, 0); +} + +function buildLegendString(config) { + var legend = '' + if (config.printSize) + legend = 'Message size in KiB, Time/message in ms, '; + legend += 'Speed in kB/s'; + return legend; +} + +function addTasks(config, stepFunc) { + for (var i = 0; + i < config.numWarmUpIterations + config.numIterations; ++i) { + // Ignore the first |config.numWarmUpIterations| iterations. + if (i == config.numWarmUpIterations) + addResultClearingTask(config); + + var multiplierIndex = 0; + for (var size = config.startSize; + size <= config.stopThreshold; + ++multiplierIndex) { + var task = stepFunc.bind( + null, + size, + config); + tasks.push(task); + size *= config.multipliers[ + multiplierIndex % config.multipliers.length]; + } + } +} + +function addResultReportingTask(config, title) { + tasks.push(function(){ + timerID = null; + config.addToSummary(title); + reportAverageData(config); + clearAverageData(); + runNextTask(config); + }); +} + +function addResultClearingTask(config) { + tasks.push(function(){ + timerID = null; + clearAverageData(); + runNextTask(config); + }); +} + +// -------------------------------- + +function sendBenchmark(config) { + config.addToLog('Send benchmark'); + config.addToLog(buildLegendString(config)); + + tasks = []; + clearAverageData(); + addTasks(config, sendBenchmarkStep); + addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config)); + startBenchmark(config); +} + +function receiveBenchmark(config) { + config.addToLog('Receive benchmark'); + config.addToLog(buildLegendString(config)); + + tasks = []; + clearAverageData(); + addTasks(config, receiveBenchmarkStep); + addResultReportingTask(config, + 'Receive Benchmark ' + getConfigString(config)); + startBenchmark(config); +} + +function batchBenchmark(originalConfig) { + originalConfig.addToLog('Batch benchmark'); + + tasks = []; + clearAverageData(); + + var dataTypes = ['text', 'blob', 'arraybuffer']; + var stepFuncs = [sendBenchmarkStep, receiveBenchmarkStep]; + var names = ['Send', 'Receive']; + var async = [true, false]; + for (var i = 0; i < stepFuncs.length; ++i) { + for (var j = 0; j < dataTypes.length; ++j) { + for (var k = 0; k < async.length; ++k) { + var config = cloneConfig(originalConfig); + config.dataType = dataTypes[j]; + config.async = async[k]; + + // Receive && Non-Worker && Sync is not supported by the spec + if (stepFuncs[i] === receiveBenchmarkStep && !isWorker && + !config.async) + continue; + + addTasks(config, stepFuncs[i]); + addResultReportingTask(config, + names[i] + ' benchmark ' + getConfigString(config)); + } + } + } + + startBenchmark(config); +} + + +function stop(config) { + destroyAllXHRs(); + clearTimeout(timerID); + timerID = null; + config.addToLog('Stopped'); +} + +onmessage = function (message) { + var config = message.data.config; + config.addToLog = workerAddToLog; + config.addToSummary = workerAddToSummary; + config.measureValue = workerMeasureValue; + if (message.data.type === 'sendBenchmark') + sendBenchmark(config); + else if (message.data.type === 'receiveBenchmark') + receiveBenchmark(config); + else if (message.data.type === 'batchBenchmark') + batchBenchmark(config); + else if (message.data.type === 'stop') + stop(config); +}; diff --git a/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_event_logger.html b/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_event_logger.html new file mode 100644 index 000000000..6983553b8 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/example/xhr_event_logger.html @@ -0,0 +1,110 @@ +<!-- +Copyright 2014 Google Inc. All rights reserved. + +Use of this source code is governed by a BSD-style +license that can be found in the COPYING file or at +https://developers.google.com/open-source/licenses/bsd +--> + +<html> +<head> +<title>XHR event logger</title> +<script src="util_main.js"></script> +<script> +var events = []; + +function run() { + events = []; + + function pushToLog(type) { + if (events.length != 0 && type === events[events.length - 1].type) { + events[events.length - 1].count += 1; + } else { + events.push({type: type, count: 1}); + } + } + + var xhr = new XMLHttpRequest(); + + function getProgressEventDump(e) { + return '(' + e.lengthComputable + ', ' + e.loaded + ', ' + e.total + ')'; + } + + var dumpProgressEvent = getBoolFromCheckBox('dumpprogressevent'); + + function log(e) { + var type = e.type; + if (type === 'readystatechange') { + type += e.target.readyState; + } + if (dumpProgressEvent && (e instanceof ProgressEvent)) { + type += getProgressEventDump(e); + } + pushToLog(type); + }; + + function logUpload(e) { + var type = e.type; + if (dumpProgressEvent && (e instanceof ProgressEvent)) { + type += getProgressEventDump(e); + } + pushToLog('upload' + type); + } + + if (getBoolFromCheckBox('upload')) { + var upload = xhr.upload; + upload.onloadstart = logUpload; + upload.onprogress = logUpload; + upload.onabort = logUpload; + upload.onerror = logUpload; + upload.onload = logUpload; + upload.ontimeout = logUpload; + upload.onloadend = logUpload; + } + + xhr.onreadystatechange = log; + xhr.onloadstart = log; + xhr.onprogress = log; + xhr.onabort = log; + xhr.onerror = log; + xhr.onload = log; + xhr.ontimeout = log; + xhr.onloadend = log; + + xhr.open('POST', '/073be001e10950692ccbf3a2ad21c245_receive', + getBoolFromCheckBox('async')); + var size = getIntFromInput('size'); + var chunkedMode = 'none'; + if (getBoolFromCheckBox('chunkedresponse')) { + chunkedMode = 'chunked'; + } + xhr.send(size + ' ' + chunkedMode); +} + +function print() { + var result = ''; + for (var i = 0; i < events.length; ++i) { + var event = events[i]; + result += event.type + ' * ' + event.count + '\n'; + } + document.getElementById('log').value = result; +} +</script> + +<body> + <textarea id="log" rows="10" cols="40" readonly></textarea> + <br/> + Size: <input type="text" id="size" value="65536"><br/> + <input type="checkbox" id="chunkedresponse"> + <label for="chunkedresponse">Use Chunked T-E for response</label><br/> + <input type="checkbox" id="upload"> + <label for="upload">Upload progress</label><br/> + <input type="checkbox" id="dumpprogressevent"> + <label for="dumpprogressevent"> + Dump lengthComputable/loaded/total</label><br/> + <input type="checkbox" id="async" checked> + <label for="async">Async</label><br/> + <input type="button" onclick="run()" value="Run XHR"> + <input type="button" onclick="print()" value="Print log"> +</body> +</html> diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/__init__.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/__init__.py new file mode 100644 index 000000000..70933a220 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/__init__.py @@ -0,0 +1,224 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""WebSocket extension for Apache HTTP Server. + +mod_pywebsocket is a WebSocket extension for Apache HTTP Server +intended for testing or experimental purposes. mod_python is required. + + +Installation +============ + +0. Prepare an Apache HTTP Server for which mod_python is enabled. + +1. Specify the following Apache HTTP Server directives to suit your + configuration. + + If mod_pywebsocket is not in the Python path, specify the following. + <websock_lib> is the directory where mod_pywebsocket is installed. + + PythonPath "sys.path+['<websock_lib>']" + + Always specify the following. <websock_handlers> is the directory where + user-written WebSocket handlers are placed. + + PythonOption mod_pywebsocket.handler_root <websock_handlers> + PythonHeaderParserHandler mod_pywebsocket.headerparserhandler + + To limit the search for WebSocket handlers to a directory <scan_dir> + under <websock_handlers>, configure as follows: + + PythonOption mod_pywebsocket.handler_scan <scan_dir> + + <scan_dir> is useful in saving scan time when <websock_handlers> + contains many non-WebSocket handler files. + + If you want to allow handlers whose canonical path is not under the root + directory (i.e. symbolic link is in root directory but its target is not), + configure as follows: + + PythonOption mod_pywebsocket.allow_handlers_outside_root_dir On + + Example snippet of httpd.conf: + (mod_pywebsocket is in /websock_lib, WebSocket handlers are in + /websock_handlers, port is 80 for ws, 443 for wss.) + + <IfModule python_module> + PythonPath "sys.path+['/websock_lib']" + PythonOption mod_pywebsocket.handler_root /websock_handlers + PythonHeaderParserHandler mod_pywebsocket.headerparserhandler + </IfModule> + +2. Tune Apache parameters for serving WebSocket. We'd like to note that at + least TimeOut directive from core features and RequestReadTimeout + directive from mod_reqtimeout should be modified not to kill connections + in only a few seconds of idle time. + +3. Verify installation. You can use example/console.html to poke the server. + + +Writing WebSocket handlers +========================== + +When a WebSocket request comes in, the resource name +specified in the handshake is considered as if it is a file path under +<websock_handlers> and the handler defined in +<websock_handlers>/<resource_name>_wsh.py is invoked. + +For example, if the resource name is /example/chat, the handler defined in +<websock_handlers>/example/chat_wsh.py is invoked. + +A WebSocket handler is composed of the following three functions: + + web_socket_do_extra_handshake(request) + web_socket_transfer_data(request) + web_socket_passive_closing_handshake(request) + +where: + request: mod_python request. + +web_socket_do_extra_handshake is called during the handshake after the +headers are successfully parsed and WebSocket properties (ws_location, +ws_origin, and ws_resource) are added to request. A handler +can reject the request by raising an exception. + +A request object has the following properties that you can use during the +extra handshake (web_socket_do_extra_handshake): +- ws_resource +- ws_origin +- ws_version +- ws_location (HyBi 00 only) +- ws_extensions (HyBi 06 and later) +- ws_deflate (HyBi 06 and later) +- ws_protocol +- ws_requested_protocols (HyBi 06 and later) + +The last two are a bit tricky. See the next subsection. + + +Subprotocol Negotiation +----------------------- + +For HyBi 06 and later, ws_protocol is always set to None when +web_socket_do_extra_handshake is called. If ws_requested_protocols is not +None, you must choose one subprotocol from this list and set it to +ws_protocol. + +For HyBi 00, when web_socket_do_extra_handshake is called, +ws_protocol is set to the value given by the client in +Sec-WebSocket-Protocol header or None if +such header was not found in the opening handshake request. Finish extra +handshake with ws_protocol untouched to accept the request subprotocol. +Then, Sec-WebSocket-Protocol header will be sent to +the client in response with the same value as requested. Raise an exception +in web_socket_do_extra_handshake to reject the requested subprotocol. + + +Data Transfer +------------- + +web_socket_transfer_data is called after the handshake completed +successfully. A handler can receive/send messages from/to the client +using request. mod_pywebsocket.msgutil module provides utilities +for data transfer. + +You can receive a message by the following statement. + + message = request.ws_stream.receive_message() + +This call blocks until any complete text frame arrives, and the payload data +of the incoming frame will be stored into message. When you're using IETF +HyBi 00 or later protocol, receive_message() will return None on receiving +client-initiated closing handshake. When any error occurs, receive_message() +will raise some exception. + +You can send a message by the following statement. + + request.ws_stream.send_message(message) + + +Closing Connection +------------------ + +Executing the following statement or just return-ing from +web_socket_transfer_data cause connection close. + + request.ws_stream.close_connection() + +close_connection will wait +for closing handshake acknowledgement coming from the client. When it +couldn't receive a valid acknowledgement, raises an exception. + +web_socket_passive_closing_handshake is called after the server receives +incoming closing frame from the client peer immediately. You can specify +code and reason by return values. They are sent as a outgoing closing frame +from the server. A request object has the following properties that you can +use in web_socket_passive_closing_handshake. +- ws_close_code +- ws_close_reason + + +Threading +--------- + +A WebSocket handler must be thread-safe if the server (Apache or +standalone.py) is configured to use threads. + + +Configuring WebSocket Extension Processors +------------------------------------------ + +See extensions.py for supported WebSocket extensions. Note that they are +unstable and their APIs are subject to change substantially. + +A request object has these extension processing related attributes. + +- ws_requested_extensions: + + A list of common.ExtensionParameter instances representing extension + parameters received from the client in the client's opening handshake. + You shouldn't modify it manually. + +- ws_extensions: + + A list of common.ExtensionParameter instances representing extension + parameters to send back to the client in the server's opening handshake. + You shouldn't touch it directly. Instead, call methods on extension + processors. + +- ws_extension_processors: + + A list of loaded extension processors. Find the processor for the + extension you want to configure from it, and call its methods. +""" + + +# vi:sts=4 sw=4 et tw=72 diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_base.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_base.py new file mode 100644 index 000000000..8235666bb --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_base.py @@ -0,0 +1,181 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Base stream class. +""" + + +# Note: request.connection.write/read are used in this module, even though +# mod_python document says that they should be used only in connection +# handlers. Unfortunately, we have no other options. For example, +# request.write/read are not suitable because they don't allow direct raw bytes +# writing/reading. + + +import socket + +from mod_pywebsocket import util + + +# Exceptions + + +class ConnectionTerminatedException(Exception): + """This exception will be raised when a connection is terminated + unexpectedly. + """ + + pass + + +class InvalidFrameException(ConnectionTerminatedException): + """This exception will be raised when we received an invalid frame we + cannot parse. + """ + + pass + + +class BadOperationException(Exception): + """This exception will be raised when send_message() is called on + server-terminated connection or receive_message() is called on + client-terminated connection. + """ + + pass + + +class UnsupportedFrameException(Exception): + """This exception will be raised when we receive a frame with flag, opcode + we cannot handle. Handlers can just catch and ignore this exception and + call receive_message() again to continue processing the next frame. + """ + + pass + + +class InvalidUTF8Exception(Exception): + """This exception will be raised when we receive a text frame which + contains invalid UTF-8 strings. + """ + + pass + + +class StreamBase(object): + """Base stream class.""" + + def __init__(self, request): + """Construct an instance. + + Args: + request: mod_python request. + """ + + self._logger = util.get_class_logger(self) + + self._request = request + + def _read(self, length): + """Reads length bytes from connection. In case we catch any exception, + prepends remote address to the exception message and raise again. + + Raises: + ConnectionTerminatedException: when read returns empty string. + """ + + try: + read_bytes = self._request.connection.read(length) + if not read_bytes: + raise ConnectionTerminatedException( + 'Receiving %d byte failed. Peer (%r) closed connection' % + (length, (self._request.connection.remote_addr,))) + return read_bytes + except socket.error, e: + # Catch a socket.error. Because it's not a child class of the + # IOError prior to Python 2.6, we cannot omit this except clause. + # Use %s rather than %r for the exception to use human friendly + # format. + raise ConnectionTerminatedException( + 'Receiving %d byte failed. socket.error (%s) occurred' % + (length, e)) + except IOError, e: + # Also catch an IOError because mod_python throws it. + raise ConnectionTerminatedException( + 'Receiving %d byte failed. IOError (%s) occurred' % + (length, e)) + + def _write(self, bytes_to_write): + """Writes given bytes to connection. In case we catch any exception, + prepends remote address to the exception message and raise again. + """ + + try: + self._request.connection.write(bytes_to_write) + except Exception, e: + util.prepend_message_to_exception( + 'Failed to send message to %r: ' % + (self._request.connection.remote_addr,), + e) + raise + + def receive_bytes(self, length): + """Receives multiple bytes. Retries read when we couldn't receive the + specified amount. + + Raises: + ConnectionTerminatedException: when read returns empty string. + """ + + read_bytes = [] + while length > 0: + new_read_bytes = self._read(length) + read_bytes.append(new_read_bytes) + length -= len(new_read_bytes) + return ''.join(read_bytes) + + def _read_until(self, delim_char): + """Reads bytes until we encounter delim_char. The result will not + contain delim_char. + + Raises: + ConnectionTerminatedException: when read returns empty string. + """ + + read_bytes = [] + while True: + ch = self._read(1) + if ch == delim_char: + break + read_bytes.append(ch) + return ''.join(read_bytes) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_hixie75.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_hixie75.py new file mode 100644 index 000000000..94cf5b31b --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_hixie75.py @@ -0,0 +1,229 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""This file provides a class for parsing/building frames of the WebSocket +protocol version HyBi 00 and Hixie 75. + +Specification: +- HyBi 00 http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00 +- Hixie 75 http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75 +""" + + +from mod_pywebsocket import common +from mod_pywebsocket._stream_base import BadOperationException +from mod_pywebsocket._stream_base import ConnectionTerminatedException +from mod_pywebsocket._stream_base import InvalidFrameException +from mod_pywebsocket._stream_base import StreamBase +from mod_pywebsocket._stream_base import UnsupportedFrameException +from mod_pywebsocket import util + + +class StreamHixie75(StreamBase): + """A class for parsing/building frames of the WebSocket protocol version + HyBi 00 and Hixie 75. + """ + + def __init__(self, request, enable_closing_handshake=False): + """Construct an instance. + + Args: + request: mod_python request. + enable_closing_handshake: to let StreamHixie75 perform closing + handshake as specified in HyBi 00, set + this option to True. + """ + + StreamBase.__init__(self, request) + + self._logger = util.get_class_logger(self) + + self._enable_closing_handshake = enable_closing_handshake + + self._request.client_terminated = False + self._request.server_terminated = False + + def send_message(self, message, end=True, binary=False): + """Send message. + + Args: + message: unicode string to send. + binary: not used in hixie75. + + Raises: + BadOperationException: when called on a server-terminated + connection. + """ + + if not end: + raise BadOperationException( + 'StreamHixie75 doesn\'t support send_message with end=False') + + if binary: + raise BadOperationException( + 'StreamHixie75 doesn\'t support send_message with binary=True') + + if self._request.server_terminated: + raise BadOperationException( + 'Requested send_message after sending out a closing handshake') + + self._write(''.join(['\x00', message.encode('utf-8'), '\xff'])) + + def _read_payload_length_hixie75(self): + """Reads a length header in a Hixie75 version frame with length. + + Raises: + ConnectionTerminatedException: when read returns empty string. + """ + + length = 0 + while True: + b_str = self._read(1) + b = ord(b_str) + length = length * 128 + (b & 0x7f) + if (b & 0x80) == 0: + break + return length + + def receive_message(self): + """Receive a WebSocket frame and return its payload an unicode string. + + Returns: + payload unicode string in a WebSocket frame. + + Raises: + ConnectionTerminatedException: when read returns empty + string. + BadOperationException: when called on a client-terminated + connection. + """ + + if self._request.client_terminated: + raise BadOperationException( + 'Requested receive_message after receiving a closing ' + 'handshake') + + while True: + # Read 1 byte. + # mp_conn.read will block if no bytes are available. + # Timeout is controlled by TimeOut directive of Apache. + frame_type_str = self.receive_bytes(1) + frame_type = ord(frame_type_str) + if (frame_type & 0x80) == 0x80: + # The payload length is specified in the frame. + # Read and discard. + length = self._read_payload_length_hixie75() + if length > 0: + _ = self.receive_bytes(length) + # 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the + # /client terminated/ flag and abort these steps. + if not self._enable_closing_handshake: + continue + + if frame_type == 0xFF and length == 0: + self._request.client_terminated = True + + if self._request.server_terminated: + self._logger.debug( + 'Received ack for server-initiated closing ' + 'handshake') + return None + + self._logger.debug( + 'Received client-initiated closing handshake') + + self._send_closing_handshake() + self._logger.debug( + 'Sent ack for client-initiated closing handshake') + return None + else: + # The payload is delimited with \xff. + bytes = self._read_until('\xff') + # The WebSocket protocol section 4.4 specifies that invalid + # characters must be replaced with U+fffd REPLACEMENT + # CHARACTER. + message = bytes.decode('utf-8', 'replace') + if frame_type == 0x00: + return message + # Discard data of other types. + + def _send_closing_handshake(self): + if not self._enable_closing_handshake: + raise BadOperationException( + 'Closing handshake is not supported in Hixie 75 protocol') + + self._request.server_terminated = True + + # 5.3 the server may decide to terminate the WebSocket connection by + # running through the following steps: + # 1. send a 0xFF byte and a 0x00 byte to the client to indicate the + # start of the closing handshake. + self._write('\xff\x00') + + def close_connection(self, unused_code='', unused_reason=''): + """Closes a WebSocket connection. + + Raises: + ConnectionTerminatedException: when closing handshake was + not successfull. + """ + + if self._request.server_terminated: + self._logger.debug( + 'Requested close_connection but server is already terminated') + return + + if not self._enable_closing_handshake: + self._request.server_terminated = True + self._logger.debug('Connection closed') + return + + self._send_closing_handshake() + self._logger.debug('Sent server-initiated closing handshake') + + # TODO(ukai): 2. wait until the /client terminated/ flag has been set, + # or until a server-defined timeout expires. + # + # For now, we expect receiving closing handshake right after sending + # out closing handshake, and if we couldn't receive non-handshake + # frame, we take it as ConnectionTerminatedException. + message = self.receive_message() + if message is not None: + raise ConnectionTerminatedException( + 'Didn\'t receive valid ack for closing handshake') + # TODO: 3. close the WebSocket connection. + # note: mod_python Connection (mp_conn) doesn't have close method. + + def send_ping(self, body): + raise BadOperationException( + 'StreamHixie75 doesn\'t support send_ping') + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_hybi.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_hybi.py new file mode 100644 index 000000000..a8a49e3c3 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/_stream_hybi.py @@ -0,0 +1,887 @@ +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""This file provides classes and helper functions for parsing/building frames +of the WebSocket protocol (RFC 6455). + +Specification: +http://tools.ietf.org/html/rfc6455 +""" + + +from collections import deque +import logging +import os +import struct +import time + +from mod_pywebsocket import common +from mod_pywebsocket import util +from mod_pywebsocket._stream_base import BadOperationException +from mod_pywebsocket._stream_base import ConnectionTerminatedException +from mod_pywebsocket._stream_base import InvalidFrameException +from mod_pywebsocket._stream_base import InvalidUTF8Exception +from mod_pywebsocket._stream_base import StreamBase +from mod_pywebsocket._stream_base import UnsupportedFrameException + + +_NOOP_MASKER = util.NoopMasker() + + +class Frame(object): + + def __init__(self, fin=1, rsv1=0, rsv2=0, rsv3=0, + opcode=None, payload=''): + self.fin = fin + self.rsv1 = rsv1 + self.rsv2 = rsv2 + self.rsv3 = rsv3 + self.opcode = opcode + self.payload = payload + + +# Helper functions made public to be used for writing unittests for WebSocket +# clients. + + +def create_length_header(length, mask): + """Creates a length header. + + Args: + length: Frame length. Must be less than 2^63. + mask: Mask bit. Must be boolean. + + Raises: + ValueError: when bad data is given. + """ + + if mask: + mask_bit = 1 << 7 + else: + mask_bit = 0 + + if length < 0: + raise ValueError('length must be non negative integer') + elif length <= 125: + return chr(mask_bit | length) + elif length < (1 << 16): + return chr(mask_bit | 126) + struct.pack('!H', length) + elif length < (1 << 63): + return chr(mask_bit | 127) + struct.pack('!Q', length) + else: + raise ValueError('Payload is too big for one frame') + + +def create_header(opcode, payload_length, fin, rsv1, rsv2, rsv3, mask): + """Creates a frame header. + + Raises: + Exception: when bad data is given. + """ + + if opcode < 0 or 0xf < opcode: + raise ValueError('Opcode out of range') + + if payload_length < 0 or (1 << 63) <= payload_length: + raise ValueError('payload_length out of range') + + if (fin | rsv1 | rsv2 | rsv3) & ~1: + raise ValueError('FIN bit and Reserved bit parameter must be 0 or 1') + + header = '' + + first_byte = ((fin << 7) + | (rsv1 << 6) | (rsv2 << 5) | (rsv3 << 4) + | opcode) + header += chr(first_byte) + header += create_length_header(payload_length, mask) + + return header + + +def _build_frame(header, body, mask): + if not mask: + return header + body + + masking_nonce = os.urandom(4) + masker = util.RepeatedXorMasker(masking_nonce) + + return header + masking_nonce + masker.mask(body) + + +def _filter_and_format_frame_object(frame, mask, frame_filters): + for frame_filter in frame_filters: + frame_filter.filter(frame) + + header = create_header( + frame.opcode, len(frame.payload), frame.fin, + frame.rsv1, frame.rsv2, frame.rsv3, mask) + return _build_frame(header, frame.payload, mask) + + +def create_binary_frame( + message, opcode=common.OPCODE_BINARY, fin=1, mask=False, frame_filters=[]): + """Creates a simple binary frame with no extension, reserved bit.""" + + frame = Frame(fin=fin, opcode=opcode, payload=message) + return _filter_and_format_frame_object(frame, mask, frame_filters) + + +def create_text_frame( + message, opcode=common.OPCODE_TEXT, fin=1, mask=False, frame_filters=[]): + """Creates a simple text frame with no extension, reserved bit.""" + + encoded_message = message.encode('utf-8') + return create_binary_frame(encoded_message, opcode, fin, mask, + frame_filters) + + +def parse_frame(receive_bytes, logger=None, + ws_version=common.VERSION_HYBI_LATEST, + unmask_receive=True): + """Parses a frame. Returns a tuple containing each header field and + payload. + + Args: + receive_bytes: a function that reads frame data from a stream or + something similar. The function takes length of the bytes to be + read. The function must raise ConnectionTerminatedException if + there is not enough data to be read. + logger: a logging object. + ws_version: the version of WebSocket protocol. + unmask_receive: unmask received frames. When received unmasked + frame, raises InvalidFrameException. + + Raises: + ConnectionTerminatedException: when receive_bytes raises it. + InvalidFrameException: when the frame contains invalid data. + """ + + if not logger: + logger = logging.getLogger() + + logger.log(common.LOGLEVEL_FINE, 'Receive the first 2 octets of a frame') + + received = receive_bytes(2) + + first_byte = ord(received[0]) + fin = (first_byte >> 7) & 1 + rsv1 = (first_byte >> 6) & 1 + rsv2 = (first_byte >> 5) & 1 + rsv3 = (first_byte >> 4) & 1 + opcode = first_byte & 0xf + + second_byte = ord(received[1]) + mask = (second_byte >> 7) & 1 + payload_length = second_byte & 0x7f + + logger.log(common.LOGLEVEL_FINE, + 'FIN=%s, RSV1=%s, RSV2=%s, RSV3=%s, opcode=%s, ' + 'Mask=%s, Payload_length=%s', + fin, rsv1, rsv2, rsv3, opcode, mask, payload_length) + + if (mask == 1) != unmask_receive: + raise InvalidFrameException( + 'Mask bit on the received frame did\'nt match masking ' + 'configuration for received frames') + + # The HyBi and later specs disallow putting a value in 0x0-0xFFFF + # into the 8-octet extended payload length field (or 0x0-0xFD in + # 2-octet field). + valid_length_encoding = True + length_encoding_bytes = 1 + if payload_length == 127: + logger.log(common.LOGLEVEL_FINE, + 'Receive 8-octet extended payload length') + + extended_payload_length = receive_bytes(8) + payload_length = struct.unpack( + '!Q', extended_payload_length)[0] + if payload_length > 0x7FFFFFFFFFFFFFFF: + raise InvalidFrameException( + 'Extended payload length >= 2^63') + if ws_version >= 13 and payload_length < 0x10000: + valid_length_encoding = False + length_encoding_bytes = 8 + + logger.log(common.LOGLEVEL_FINE, + 'Decoded_payload_length=%s', payload_length) + elif payload_length == 126: + logger.log(common.LOGLEVEL_FINE, + 'Receive 2-octet extended payload length') + + extended_payload_length = receive_bytes(2) + payload_length = struct.unpack( + '!H', extended_payload_length)[0] + if ws_version >= 13 and payload_length < 126: + valid_length_encoding = False + length_encoding_bytes = 2 + + logger.log(common.LOGLEVEL_FINE, + 'Decoded_payload_length=%s', payload_length) + + if not valid_length_encoding: + logger.warning( + 'Payload length is not encoded using the minimal number of ' + 'bytes (%d is encoded using %d bytes)', + payload_length, + length_encoding_bytes) + + if mask == 1: + logger.log(common.LOGLEVEL_FINE, 'Receive mask') + + masking_nonce = receive_bytes(4) + masker = util.RepeatedXorMasker(masking_nonce) + + logger.log(common.LOGLEVEL_FINE, 'Mask=%r', masking_nonce) + else: + masker = _NOOP_MASKER + + logger.log(common.LOGLEVEL_FINE, 'Receive payload data') + if logger.isEnabledFor(common.LOGLEVEL_FINE): + receive_start = time.time() + + raw_payload_bytes = receive_bytes(payload_length) + + if logger.isEnabledFor(common.LOGLEVEL_FINE): + logger.log( + common.LOGLEVEL_FINE, + 'Done receiving payload data at %s MB/s', + payload_length / (time.time() - receive_start) / 1000 / 1000) + logger.log(common.LOGLEVEL_FINE, 'Unmask payload data') + + if logger.isEnabledFor(common.LOGLEVEL_FINE): + unmask_start = time.time() + + unmasked_bytes = masker.mask(raw_payload_bytes) + + if logger.isEnabledFor(common.LOGLEVEL_FINE): + logger.log( + common.LOGLEVEL_FINE, + 'Done unmasking payload data at %s MB/s', + payload_length / (time.time() - unmask_start) / 1000 / 1000) + + return opcode, unmasked_bytes, fin, rsv1, rsv2, rsv3 + + +class FragmentedFrameBuilder(object): + """A stateful class to send a message as fragments.""" + + def __init__(self, mask, frame_filters=[], encode_utf8=True): + """Constructs an instance.""" + + self._mask = mask + self._frame_filters = frame_filters + # This is for skipping UTF-8 encoding when building text type frames + # from compressed data. + self._encode_utf8 = encode_utf8 + + self._started = False + + # Hold opcode of the first frame in messages to verify types of other + # frames in the message are all the same. + self._opcode = common.OPCODE_TEXT + + def build(self, payload_data, end, binary): + if binary: + frame_type = common.OPCODE_BINARY + else: + frame_type = common.OPCODE_TEXT + if self._started: + if self._opcode != frame_type: + raise ValueError('Message types are different in frames for ' + 'the same message') + opcode = common.OPCODE_CONTINUATION + else: + opcode = frame_type + self._opcode = frame_type + + if end: + self._started = False + fin = 1 + else: + self._started = True + fin = 0 + + if binary or not self._encode_utf8: + return create_binary_frame( + payload_data, opcode, fin, self._mask, self._frame_filters) + else: + return create_text_frame( + payload_data, opcode, fin, self._mask, self._frame_filters) + + +def _create_control_frame(opcode, body, mask, frame_filters): + frame = Frame(opcode=opcode, payload=body) + + for frame_filter in frame_filters: + frame_filter.filter(frame) + + if len(frame.payload) > 125: + raise BadOperationException( + 'Payload data size of control frames must be 125 bytes or less') + + header = create_header( + frame.opcode, len(frame.payload), frame.fin, + frame.rsv1, frame.rsv2, frame.rsv3, mask) + return _build_frame(header, frame.payload, mask) + + +def create_ping_frame(body, mask=False, frame_filters=[]): + return _create_control_frame(common.OPCODE_PING, body, mask, frame_filters) + + +def create_pong_frame(body, mask=False, frame_filters=[]): + return _create_control_frame(common.OPCODE_PONG, body, mask, frame_filters) + + +def create_close_frame(body, mask=False, frame_filters=[]): + return _create_control_frame( + common.OPCODE_CLOSE, body, mask, frame_filters) + + +def create_closing_handshake_body(code, reason): + body = '' + if code is not None: + if (code > common.STATUS_USER_PRIVATE_MAX or + code < common.STATUS_NORMAL_CLOSURE): + raise BadOperationException('Status code is out of range') + if (code == common.STATUS_NO_STATUS_RECEIVED or + code == common.STATUS_ABNORMAL_CLOSURE or + code == common.STATUS_TLS_HANDSHAKE): + raise BadOperationException('Status code is reserved pseudo ' + 'code') + encoded_reason = reason.encode('utf-8') + body = struct.pack('!H', code) + encoded_reason + return body + + +class StreamOptions(object): + """Holds option values to configure Stream objects.""" + + def __init__(self): + """Constructs StreamOptions.""" + + # Filters applied to frames. + self.outgoing_frame_filters = [] + self.incoming_frame_filters = [] + + # Filters applied to messages. Control frames are not affected by them. + self.outgoing_message_filters = [] + self.incoming_message_filters = [] + + self.encode_text_message_to_utf8 = True + self.mask_send = False + self.unmask_receive = True + + +class Stream(StreamBase): + """A class for parsing/building frames of the WebSocket protocol + (RFC 6455). + """ + + def __init__(self, request, options): + """Constructs an instance. + + Args: + request: mod_python request. + """ + + StreamBase.__init__(self, request) + + self._logger = util.get_class_logger(self) + + self._options = options + + self._request.client_terminated = False + self._request.server_terminated = False + + # Holds body of received fragments. + self._received_fragments = [] + # Holds the opcode of the first fragment. + self._original_opcode = None + + self._writer = FragmentedFrameBuilder( + self._options.mask_send, self._options.outgoing_frame_filters, + self._options.encode_text_message_to_utf8) + + self._ping_queue = deque() + + def _receive_frame(self): + """Receives a frame and return data in the frame as a tuple containing + each header field and payload separately. + + Raises: + ConnectionTerminatedException: when read returns empty + string. + InvalidFrameException: when the frame contains invalid data. + """ + + def _receive_bytes(length): + return self.receive_bytes(length) + + return parse_frame(receive_bytes=_receive_bytes, + logger=self._logger, + ws_version=self._request.ws_version, + unmask_receive=self._options.unmask_receive) + + def _receive_frame_as_frame_object(self): + opcode, unmasked_bytes, fin, rsv1, rsv2, rsv3 = self._receive_frame() + + return Frame(fin=fin, rsv1=rsv1, rsv2=rsv2, rsv3=rsv3, + opcode=opcode, payload=unmasked_bytes) + + def receive_filtered_frame(self): + """Receives a frame and applies frame filters and message filters. + The frame to be received must satisfy following conditions: + - The frame is not fragmented. + - The opcode of the frame is TEXT or BINARY. + + DO NOT USE this method except for testing purpose. + """ + + frame = self._receive_frame_as_frame_object() + if not frame.fin: + raise InvalidFrameException( + 'Segmented frames must not be received via ' + 'receive_filtered_frame()') + if (frame.opcode != common.OPCODE_TEXT and + frame.opcode != common.OPCODE_BINARY): + raise InvalidFrameException( + 'Control frames must not be received via ' + 'receive_filtered_frame()') + + for frame_filter in self._options.incoming_frame_filters: + frame_filter.filter(frame) + for message_filter in self._options.incoming_message_filters: + frame.payload = message_filter.filter(frame.payload) + return frame + + def send_message(self, message, end=True, binary=False): + """Send message. + + Args: + message: text in unicode or binary in str to send. + binary: send message as binary frame. + + Raises: + BadOperationException: when called on a server-terminated + connection or called with inconsistent message type or + binary parameter. + """ + + if self._request.server_terminated: + raise BadOperationException( + 'Requested send_message after sending out a closing handshake') + + if binary and isinstance(message, unicode): + raise BadOperationException( + 'Message for binary frame must be instance of str') + + for message_filter in self._options.outgoing_message_filters: + message = message_filter.filter(message, end, binary) + + try: + # Set this to any positive integer to limit maximum size of data in + # payload data of each frame. + MAX_PAYLOAD_DATA_SIZE = -1 + + if MAX_PAYLOAD_DATA_SIZE <= 0: + self._write(self._writer.build(message, end, binary)) + return + + bytes_written = 0 + while True: + end_for_this_frame = end + bytes_to_write = len(message) - bytes_written + if (MAX_PAYLOAD_DATA_SIZE > 0 and + bytes_to_write > MAX_PAYLOAD_DATA_SIZE): + end_for_this_frame = False + bytes_to_write = MAX_PAYLOAD_DATA_SIZE + + frame = self._writer.build( + message[bytes_written:bytes_written + bytes_to_write], + end_for_this_frame, + binary) + self._write(frame) + + bytes_written += bytes_to_write + + # This if must be placed here (the end of while block) so that + # at least one frame is sent. + if len(message) <= bytes_written: + break + except ValueError, e: + raise BadOperationException(e) + + def _get_message_from_frame(self, frame): + """Gets a message from frame. If the message is composed of fragmented + frames and the frame is not the last fragmented frame, this method + returns None. The whole message will be returned when the last + fragmented frame is passed to this method. + + Raises: + InvalidFrameException: when the frame doesn't match defragmentation + context, or the frame contains invalid data. + """ + + if frame.opcode == common.OPCODE_CONTINUATION: + if not self._received_fragments: + if frame.fin: + raise InvalidFrameException( + 'Received a termination frame but fragmentation ' + 'not started') + else: + raise InvalidFrameException( + 'Received an intermediate frame but ' + 'fragmentation not started') + + if frame.fin: + # End of fragmentation frame + self._received_fragments.append(frame.payload) + message = ''.join(self._received_fragments) + self._received_fragments = [] + return message + else: + # Intermediate frame + self._received_fragments.append(frame.payload) + return None + else: + if self._received_fragments: + if frame.fin: + raise InvalidFrameException( + 'Received an unfragmented frame without ' + 'terminating existing fragmentation') + else: + raise InvalidFrameException( + 'New fragmentation started without terminating ' + 'existing fragmentation') + + if frame.fin: + # Unfragmented frame + + self._original_opcode = frame.opcode + return frame.payload + else: + # Start of fragmentation frame + + if common.is_control_opcode(frame.opcode): + raise InvalidFrameException( + 'Control frames must not be fragmented') + + self._original_opcode = frame.opcode + self._received_fragments.append(frame.payload) + return None + + def _process_close_message(self, message): + """Processes close message. + + Args: + message: close message. + + Raises: + InvalidFrameException: when the message is invalid. + """ + + self._request.client_terminated = True + + # Status code is optional. We can have status reason only if we + # have status code. Status reason can be empty string. So, + # allowed cases are + # - no application data: no code no reason + # - 2 octet of application data: has code but no reason + # - 3 or more octet of application data: both code and reason + if len(message) == 0: + self._logger.debug('Received close frame (empty body)') + self._request.ws_close_code = ( + common.STATUS_NO_STATUS_RECEIVED) + elif len(message) == 1: + raise InvalidFrameException( + 'If a close frame has status code, the length of ' + 'status code must be 2 octet') + elif len(message) >= 2: + self._request.ws_close_code = struct.unpack( + '!H', message[0:2])[0] + self._request.ws_close_reason = message[2:].decode( + 'utf-8', 'replace') + self._logger.debug( + 'Received close frame (code=%d, reason=%r)', + self._request.ws_close_code, + self._request.ws_close_reason) + + # As we've received a close frame, no more data is coming over the + # socket. We can now safely close the socket without worrying about + # RST sending. + + if self._request.server_terminated: + self._logger.debug( + 'Received ack for server-initiated closing handshake') + return + + self._logger.debug( + 'Received client-initiated closing handshake') + + code = common.STATUS_NORMAL_CLOSURE + reason = '' + if hasattr(self._request, '_dispatcher'): + dispatcher = self._request._dispatcher + code, reason = dispatcher.passive_closing_handshake( + self._request) + if code is None and reason is not None and len(reason) > 0: + self._logger.warning( + 'Handler specified reason despite code being None') + reason = '' + if reason is None: + reason = '' + self._send_closing_handshake(code, reason) + self._logger.debug( + 'Acknowledged closing handshake initiated by the peer ' + '(code=%r, reason=%r)', code, reason) + + def _process_ping_message(self, message): + """Processes ping message. + + Args: + message: ping message. + """ + + try: + handler = self._request.on_ping_handler + if handler: + handler(self._request, message) + return + except AttributeError, e: + pass + self._send_pong(message) + + def _process_pong_message(self, message): + """Processes pong message. + + Args: + message: pong message. + """ + + # TODO(tyoshino): Add ping timeout handling. + + inflight_pings = deque() + + while True: + try: + expected_body = self._ping_queue.popleft() + if expected_body == message: + # inflight_pings contains pings ignored by the + # other peer. Just forget them. + self._logger.debug( + 'Ping %r is acked (%d pings were ignored)', + expected_body, len(inflight_pings)) + break + else: + inflight_pings.append(expected_body) + except IndexError, e: + # The received pong was unsolicited pong. Keep the + # ping queue as is. + self._ping_queue = inflight_pings + self._logger.debug('Received a unsolicited pong') + break + + try: + handler = self._request.on_pong_handler + if handler: + handler(self._request, message) + except AttributeError, e: + pass + + def receive_message(self): + """Receive a WebSocket frame and return its payload as a text in + unicode or a binary in str. + + Returns: + payload data of the frame + - as unicode instance if received text frame + - as str instance if received binary frame + or None iff received closing handshake. + Raises: + BadOperationException: when called on a client-terminated + connection. + ConnectionTerminatedException: when read returns empty + string. + InvalidFrameException: when the frame contains invalid + data. + UnsupportedFrameException: when the received frame has + flags, opcode we cannot handle. You can ignore this + exception and continue receiving the next frame. + """ + + if self._request.client_terminated: + raise BadOperationException( + 'Requested receive_message after receiving a closing ' + 'handshake') + + while True: + # mp_conn.read will block if no bytes are available. + # Timeout is controlled by TimeOut directive of Apache. + + frame = self._receive_frame_as_frame_object() + + # Check the constraint on the payload size for control frames + # before extension processes the frame. + # See also http://tools.ietf.org/html/rfc6455#section-5.5 + if (common.is_control_opcode(frame.opcode) and + len(frame.payload) > 125): + raise InvalidFrameException( + 'Payload data size of control frames must be 125 bytes or ' + 'less') + + for frame_filter in self._options.incoming_frame_filters: + frame_filter.filter(frame) + + if frame.rsv1 or frame.rsv2 or frame.rsv3: + raise UnsupportedFrameException( + 'Unsupported flag is set (rsv = %d%d%d)' % + (frame.rsv1, frame.rsv2, frame.rsv3)) + + message = self._get_message_from_frame(frame) + if message is None: + continue + + for message_filter in self._options.incoming_message_filters: + message = message_filter.filter(message) + + if self._original_opcode == common.OPCODE_TEXT: + # The WebSocket protocol section 4.4 specifies that invalid + # characters must be replaced with U+fffd REPLACEMENT + # CHARACTER. + try: + return message.decode('utf-8') + except UnicodeDecodeError, e: + raise InvalidUTF8Exception(e) + elif self._original_opcode == common.OPCODE_BINARY: + return message + elif self._original_opcode == common.OPCODE_CLOSE: + self._process_close_message(message) + return None + elif self._original_opcode == common.OPCODE_PING: + self._process_ping_message(message) + elif self._original_opcode == common.OPCODE_PONG: + self._process_pong_message(message) + else: + raise UnsupportedFrameException( + 'Opcode %d is not supported' % self._original_opcode) + + def _send_closing_handshake(self, code, reason): + body = create_closing_handshake_body(code, reason) + frame = create_close_frame( + body, mask=self._options.mask_send, + frame_filters=self._options.outgoing_frame_filters) + + self._request.server_terminated = True + + self._write(frame) + + def close_connection(self, code=common.STATUS_NORMAL_CLOSURE, reason='', + wait_response=True): + """Closes a WebSocket connection. + + Args: + code: Status code for close frame. If code is None, a close + frame with empty body will be sent. + reason: string representing close reason. + wait_response: True when caller want to wait the response. + Raises: + BadOperationException: when reason is specified with code None + or reason is not an instance of both str and unicode. + """ + + if self._request.server_terminated: + self._logger.debug( + 'Requested close_connection but server is already terminated') + return + + if code is None: + if reason is not None and len(reason) > 0: + raise BadOperationException( + 'close reason must not be specified if code is None') + reason = '' + else: + if not isinstance(reason, str) and not isinstance(reason, unicode): + raise BadOperationException( + 'close reason must be an instance of str or unicode') + + self._send_closing_handshake(code, reason) + self._logger.debug( + 'Initiated closing handshake (code=%r, reason=%r)', + code, reason) + + if (code == common.STATUS_GOING_AWAY or + code == common.STATUS_PROTOCOL_ERROR) or not wait_response: + # It doesn't make sense to wait for a close frame if the reason is + # protocol error or that the server is going away. For some of + # other reasons, it might not make sense to wait for a close frame, + # but it's not clear, yet. + return + + # TODO(ukai): 2. wait until the /client terminated/ flag has been set, + # or until a server-defined timeout expires. + # + # For now, we expect receiving closing handshake right after sending + # out closing handshake. + message = self.receive_message() + if message is not None: + raise ConnectionTerminatedException( + 'Didn\'t receive valid ack for closing handshake') + # TODO: 3. close the WebSocket connection. + # note: mod_python Connection (mp_conn) doesn't have close method. + + def send_ping(self, body=''): + frame = create_ping_frame( + body, + self._options.mask_send, + self._options.outgoing_frame_filters) + self._write(frame) + + self._ping_queue.append(body) + + def _send_pong(self, body): + frame = create_pong_frame( + body, + self._options.mask_send, + self._options.outgoing_frame_filters) + self._write(frame) + + def get_last_received_opcode(self): + """Returns the opcode of the WebSocket message which the last received + frame belongs to. The return value is valid iff immediately after + receive_message call. + """ + + return self._original_opcode + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/common.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/common.py new file mode 100644 index 000000000..2fc2ead64 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/common.py @@ -0,0 +1,303 @@ +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""This file must not depend on any module specific to the WebSocket protocol. +""" + + +from mod_pywebsocket import http_header_util + + +# Additional log level definitions. +LOGLEVEL_FINE = 9 + +# Constants indicating WebSocket protocol version. +VERSION_HIXIE75 = -1 +VERSION_HYBI00 = 0 +VERSION_HYBI01 = 1 +VERSION_HYBI02 = 2 +VERSION_HYBI03 = 2 +VERSION_HYBI04 = 4 +VERSION_HYBI05 = 5 +VERSION_HYBI06 = 6 +VERSION_HYBI07 = 7 +VERSION_HYBI08 = 8 +VERSION_HYBI09 = 8 +VERSION_HYBI10 = 8 +VERSION_HYBI11 = 8 +VERSION_HYBI12 = 8 +VERSION_HYBI13 = 13 +VERSION_HYBI14 = 13 +VERSION_HYBI15 = 13 +VERSION_HYBI16 = 13 +VERSION_HYBI17 = 13 + +# Constants indicating WebSocket protocol latest version. +VERSION_HYBI_LATEST = VERSION_HYBI13 + +# Port numbers +DEFAULT_WEB_SOCKET_PORT = 80 +DEFAULT_WEB_SOCKET_SECURE_PORT = 443 + +# Schemes +WEB_SOCKET_SCHEME = 'ws' +WEB_SOCKET_SECURE_SCHEME = 'wss' + +# Frame opcodes defined in the spec. +OPCODE_CONTINUATION = 0x0 +OPCODE_TEXT = 0x1 +OPCODE_BINARY = 0x2 +OPCODE_CLOSE = 0x8 +OPCODE_PING = 0x9 +OPCODE_PONG = 0xa + +# UUIDs used by HyBi 04 and later opening handshake and frame masking. +WEBSOCKET_ACCEPT_UUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + +# Opening handshake header names and expected values. +UPGRADE_HEADER = 'Upgrade' +WEBSOCKET_UPGRADE_TYPE = 'websocket' +WEBSOCKET_UPGRADE_TYPE_HIXIE75 = 'WebSocket' +CONNECTION_HEADER = 'Connection' +UPGRADE_CONNECTION_TYPE = 'Upgrade' +HOST_HEADER = 'Host' +ORIGIN_HEADER = 'Origin' +SEC_WEBSOCKET_ORIGIN_HEADER = 'Sec-WebSocket-Origin' +SEC_WEBSOCKET_KEY_HEADER = 'Sec-WebSocket-Key' +SEC_WEBSOCKET_ACCEPT_HEADER = 'Sec-WebSocket-Accept' +SEC_WEBSOCKET_VERSION_HEADER = 'Sec-WebSocket-Version' +SEC_WEBSOCKET_PROTOCOL_HEADER = 'Sec-WebSocket-Protocol' +SEC_WEBSOCKET_EXTENSIONS_HEADER = 'Sec-WebSocket-Extensions' +SEC_WEBSOCKET_DRAFT_HEADER = 'Sec-WebSocket-Draft' +SEC_WEBSOCKET_KEY1_HEADER = 'Sec-WebSocket-Key1' +SEC_WEBSOCKET_KEY2_HEADER = 'Sec-WebSocket-Key2' +SEC_WEBSOCKET_LOCATION_HEADER = 'Sec-WebSocket-Location' + +# Extensions +DEFLATE_FRAME_EXTENSION = 'deflate-frame' +PERMESSAGE_COMPRESSION_EXTENSION = 'permessage-compress' +PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate' +X_WEBKIT_DEFLATE_FRAME_EXTENSION = 'x-webkit-deflate-frame' +X_WEBKIT_PERMESSAGE_COMPRESSION_EXTENSION = 'x-webkit-permessage-compress' +MUX_EXTENSION = 'mux_DO_NOT_USE' + +# Status codes +# Code STATUS_NO_STATUS_RECEIVED, STATUS_ABNORMAL_CLOSURE, and +# STATUS_TLS_HANDSHAKE are pseudo codes to indicate specific error cases. +# Could not be used for codes in actual closing frames. +# Application level errors must use codes in the range +# STATUS_USER_REGISTERED_BASE to STATUS_USER_PRIVATE_MAX. The codes in the +# range STATUS_USER_REGISTERED_BASE to STATUS_USER_REGISTERED_MAX are managed +# by IANA. Usually application must define user protocol level errors in the +# range STATUS_USER_PRIVATE_BASE to STATUS_USER_PRIVATE_MAX. +STATUS_NORMAL_CLOSURE = 1000 +STATUS_GOING_AWAY = 1001 +STATUS_PROTOCOL_ERROR = 1002 +STATUS_UNSUPPORTED_DATA = 1003 +STATUS_NO_STATUS_RECEIVED = 1005 +STATUS_ABNORMAL_CLOSURE = 1006 +STATUS_INVALID_FRAME_PAYLOAD_DATA = 1007 +STATUS_POLICY_VIOLATION = 1008 +STATUS_MESSAGE_TOO_BIG = 1009 +STATUS_MANDATORY_EXTENSION = 1010 +STATUS_INTERNAL_ENDPOINT_ERROR = 1011 +STATUS_TLS_HANDSHAKE = 1015 +STATUS_USER_REGISTERED_BASE = 3000 +STATUS_USER_REGISTERED_MAX = 3999 +STATUS_USER_PRIVATE_BASE = 4000 +STATUS_USER_PRIVATE_MAX = 4999 +# Following definitions are aliases to keep compatibility. Applications must +# not use these obsoleted definitions anymore. +STATUS_NORMAL = STATUS_NORMAL_CLOSURE +STATUS_UNSUPPORTED = STATUS_UNSUPPORTED_DATA +STATUS_CODE_NOT_AVAILABLE = STATUS_NO_STATUS_RECEIVED +STATUS_ABNORMAL_CLOSE = STATUS_ABNORMAL_CLOSURE +STATUS_INVALID_FRAME_PAYLOAD = STATUS_INVALID_FRAME_PAYLOAD_DATA +STATUS_MANDATORY_EXT = STATUS_MANDATORY_EXTENSION + +# HTTP status codes +HTTP_STATUS_BAD_REQUEST = 400 +HTTP_STATUS_FORBIDDEN = 403 +HTTP_STATUS_NOT_FOUND = 404 + + +def is_control_opcode(opcode): + return (opcode >> 3) == 1 + + +class ExtensionParameter(object): + """Holds information about an extension which is exchanged on extension + negotiation in opening handshake. + """ + + def __init__(self, name): + self._name = name + # TODO(tyoshino): Change the data structure to more efficient one such + # as dict when the spec changes to say like + # - Parameter names must be unique + # - The order of parameters is not significant + self._parameters = [] + + def name(self): + return self._name + + def add_parameter(self, name, value): + self._parameters.append((name, value)) + + def get_parameters(self): + return self._parameters + + def get_parameter_names(self): + return [name for name, unused_value in self._parameters] + + def has_parameter(self, name): + for param_name, param_value in self._parameters: + if param_name == name: + return True + return False + + def get_parameter_value(self, name): + for param_name, param_value in self._parameters: + if param_name == name: + return param_value + + +class ExtensionParsingException(Exception): + def __init__(self, name): + super(ExtensionParsingException, self).__init__(name) + + +def _parse_extension_param(state, definition): + param_name = http_header_util.consume_token(state) + + if param_name is None: + raise ExtensionParsingException('No valid parameter name found') + + http_header_util.consume_lwses(state) + + if not http_header_util.consume_string(state, '='): + definition.add_parameter(param_name, None) + return + + http_header_util.consume_lwses(state) + + # TODO(tyoshino): Add code to validate that parsed param_value is token + param_value = http_header_util.consume_token_or_quoted_string(state) + if param_value is None: + raise ExtensionParsingException( + 'No valid parameter value found on the right-hand side of ' + 'parameter %r' % param_name) + + definition.add_parameter(param_name, param_value) + + +def _parse_extension(state): + extension_token = http_header_util.consume_token(state) + if extension_token is None: + return None + + extension = ExtensionParameter(extension_token) + + while True: + http_header_util.consume_lwses(state) + + if not http_header_util.consume_string(state, ';'): + break + + http_header_util.consume_lwses(state) + + try: + _parse_extension_param(state, extension) + except ExtensionParsingException, e: + raise ExtensionParsingException( + 'Failed to parse parameter for %r (%r)' % + (extension_token, e)) + + return extension + + +def parse_extensions(data): + """Parses Sec-WebSocket-Extensions header value returns a list of + ExtensionParameter objects. + + Leading LWSes must be trimmed. + """ + + state = http_header_util.ParsingState(data) + + extension_list = [] + while True: + extension = _parse_extension(state) + if extension is not None: + extension_list.append(extension) + + http_header_util.consume_lwses(state) + + if http_header_util.peek(state) is None: + break + + if not http_header_util.consume_string(state, ','): + raise ExtensionParsingException( + 'Failed to parse Sec-WebSocket-Extensions header: ' + 'Expected a comma but found %r' % + http_header_util.peek(state)) + + http_header_util.consume_lwses(state) + + if len(extension_list) == 0: + raise ExtensionParsingException( + 'No valid extension entry found') + + return extension_list + + +def format_extension(extension): + """Formats an ExtensionParameter object.""" + + formatted_params = [extension.name()] + for param_name, param_value in extension.get_parameters(): + if param_value is None: + formatted_params.append(param_name) + else: + quoted_value = http_header_util.quote_if_necessary(param_value) + formatted_params.append('%s=%s' % (param_name, quoted_value)) + return '; '.join(formatted_params) + + +def format_extensions(extension_list): + """Formats a list of ExtensionParameter objects.""" + + formatted_extension_list = [] + for extension in extension_list: + formatted_extension_list.append(format_extension(extension)) + return ', '.join(formatted_extension_list) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/dispatch.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/dispatch.py new file mode 100644 index 000000000..96c91e0c9 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/dispatch.py @@ -0,0 +1,393 @@ +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Dispatch WebSocket request. +""" + + +import logging +import os +import re + +from mod_pywebsocket import common +from mod_pywebsocket import handshake +from mod_pywebsocket import msgutil +from mod_pywebsocket import mux +from mod_pywebsocket import stream +from mod_pywebsocket import util + + +_SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$') +_SOURCE_SUFFIX = '_wsh.py' +_DO_EXTRA_HANDSHAKE_HANDLER_NAME = 'web_socket_do_extra_handshake' +_TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data' +_PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME = ( + 'web_socket_passive_closing_handshake') + + +class DispatchException(Exception): + """Exception in dispatching WebSocket request.""" + + def __init__(self, name, status=common.HTTP_STATUS_NOT_FOUND): + super(DispatchException, self).__init__(name) + self.status = status + + +def _default_passive_closing_handshake_handler(request): + """Default web_socket_passive_closing_handshake handler.""" + + return common.STATUS_NORMAL_CLOSURE, '' + + +def _normalize_path(path): + """Normalize path. + + Args: + path: the path to normalize. + + Path is converted to the absolute path. + The input path can use either '\\' or '/' as the separator. + The normalized path always uses '/' regardless of the platform. + """ + + path = path.replace('\\', os.path.sep) + path = os.path.realpath(path) + path = path.replace('\\', '/') + return path + + +def _create_path_to_resource_converter(base_dir): + """Returns a function that converts the path of a WebSocket handler source + file to a resource string by removing the path to the base directory from + its head, removing _SOURCE_SUFFIX from its tail, and replacing path + separators in it with '/'. + + Args: + base_dir: the path to the base directory. + """ + + base_dir = _normalize_path(base_dir) + + base_len = len(base_dir) + suffix_len = len(_SOURCE_SUFFIX) + + def converter(path): + if not path.endswith(_SOURCE_SUFFIX): + return None + # _normalize_path must not be used because resolving symlink breaks + # following path check. + path = path.replace('\\', '/') + if not path.startswith(base_dir): + return None + return path[base_len:-suffix_len] + + return converter + + +def _enumerate_handler_file_paths(directory): + """Returns a generator that enumerates WebSocket Handler source file names + in the given directory. + """ + + for root, unused_dirs, files in os.walk(directory): + for base in files: + path = os.path.join(root, base) + if _SOURCE_PATH_PATTERN.search(path): + yield path + + +class _HandlerSuite(object): + """A handler suite holder class.""" + + def __init__(self, do_extra_handshake, transfer_data, + passive_closing_handshake): + self.do_extra_handshake = do_extra_handshake + self.transfer_data = transfer_data + self.passive_closing_handshake = passive_closing_handshake + + +def _source_handler_file(handler_definition): + """Source a handler definition string. + + Args: + handler_definition: a string containing Python statements that define + handler functions. + """ + + global_dic = {} + try: + exec handler_definition in global_dic + except Exception: + raise DispatchException('Error in sourcing handler:' + + util.get_stack_trace()) + passive_closing_handshake_handler = None + try: + passive_closing_handshake_handler = _extract_handler( + global_dic, _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME) + except Exception: + passive_closing_handshake_handler = ( + _default_passive_closing_handshake_handler) + return _HandlerSuite( + _extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME), + _extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME), + passive_closing_handshake_handler) + + +def _extract_handler(dic, name): + """Extracts a callable with the specified name from the given dictionary + dic. + """ + + if name not in dic: + raise DispatchException('%s is not defined.' % name) + handler = dic[name] + if not callable(handler): + raise DispatchException('%s is not callable.' % name) + return handler + + +class Dispatcher(object): + """Dispatches WebSocket requests. + + This class maintains a map from resource name to handlers. + """ + + def __init__( + self, root_dir, scan_dir=None, + allow_handlers_outside_root_dir=True): + """Construct an instance. + + Args: + root_dir: The directory where handler definition files are + placed. + scan_dir: The directory where handler definition files are + searched. scan_dir must be a directory under root_dir, + including root_dir itself. If scan_dir is None, + root_dir is used as scan_dir. scan_dir can be useful + in saving scan time when root_dir contains many + subdirectories. + allow_handlers_outside_root_dir: Scans handler files even if their + canonical path is not under root_dir. + """ + + self._logger = util.get_class_logger(self) + + self._handler_suite_map = {} + self._source_warnings = [] + if scan_dir is None: + scan_dir = root_dir + if not os.path.realpath(scan_dir).startswith( + os.path.realpath(root_dir)): + raise DispatchException('scan_dir:%s must be a directory under ' + 'root_dir:%s.' % (scan_dir, root_dir)) + self._source_handler_files_in_dir( + root_dir, scan_dir, allow_handlers_outside_root_dir) + + def add_resource_path_alias(self, + alias_resource_path, existing_resource_path): + """Add resource path alias. + + Once added, request to alias_resource_path would be handled by + handler registered for existing_resource_path. + + Args: + alias_resource_path: alias resource path + existing_resource_path: existing resource path + """ + try: + handler_suite = self._handler_suite_map[existing_resource_path] + self._handler_suite_map[alias_resource_path] = handler_suite + except KeyError: + raise DispatchException('No handler for: %r' % + existing_resource_path) + + def source_warnings(self): + """Return warnings in sourcing handlers.""" + + return self._source_warnings + + def do_extra_handshake(self, request): + """Do extra checking in WebSocket handshake. + + Select a handler based on request.uri and call its + web_socket_do_extra_handshake function. + + Args: + request: mod_python request. + + Raises: + DispatchException: when handler was not found + AbortedByUserException: when user handler abort connection + HandshakeException: when opening handshake failed + """ + + handler_suite = self.get_handler_suite(request.ws_resource) + if handler_suite is None: + raise DispatchException('No handler for: %r' % request.ws_resource) + do_extra_handshake_ = handler_suite.do_extra_handshake + try: + do_extra_handshake_(request) + except handshake.AbortedByUserException, e: + # Re-raise to tell the caller of this function to finish this + # connection without sending any error. + self._logger.debug('%s', util.get_stack_trace()) + raise + except Exception, e: + util.prepend_message_to_exception( + '%s raised exception for %s: ' % ( + _DO_EXTRA_HANDSHAKE_HANDLER_NAME, + request.ws_resource), + e) + raise handshake.HandshakeException(e, common.HTTP_STATUS_FORBIDDEN) + + def transfer_data(self, request): + """Let a handler transfer_data with a WebSocket client. + + Select a handler based on request.ws_resource and call its + web_socket_transfer_data function. + + Args: + request: mod_python request. + + Raises: + DispatchException: when handler was not found + AbortedByUserException: when user handler abort connection + """ + + # TODO(tyoshino): Terminate underlying TCP connection if possible. + try: + if mux.use_mux(request): + mux.start(request, self) + else: + handler_suite = self.get_handler_suite(request.ws_resource) + if handler_suite is None: + raise DispatchException('No handler for: %r' % + request.ws_resource) + transfer_data_ = handler_suite.transfer_data + transfer_data_(request) + + if not request.server_terminated: + request.ws_stream.close_connection() + # Catch non-critical exceptions the handler didn't handle. + except handshake.AbortedByUserException, e: + self._logger.debug('%s', util.get_stack_trace()) + raise + except msgutil.BadOperationException, e: + self._logger.debug('%s', e) + request.ws_stream.close_connection( + common.STATUS_INTERNAL_ENDPOINT_ERROR) + except msgutil.InvalidFrameException, e: + # InvalidFrameException must be caught before + # ConnectionTerminatedException that catches InvalidFrameException. + self._logger.debug('%s', e) + request.ws_stream.close_connection(common.STATUS_PROTOCOL_ERROR) + except msgutil.UnsupportedFrameException, e: + self._logger.debug('%s', e) + request.ws_stream.close_connection(common.STATUS_UNSUPPORTED_DATA) + except stream.InvalidUTF8Exception, e: + self._logger.debug('%s', e) + request.ws_stream.close_connection( + common.STATUS_INVALID_FRAME_PAYLOAD_DATA) + except msgutil.ConnectionTerminatedException, e: + self._logger.debug('%s', e) + except Exception, e: + # Any other exceptions are forwarded to the caller of this + # function. + util.prepend_message_to_exception( + '%s raised exception for %s: ' % ( + _TRANSFER_DATA_HANDLER_NAME, request.ws_resource), + e) + raise + + def passive_closing_handshake(self, request): + """Prepare code and reason for responding client initiated closing + handshake. + """ + + handler_suite = self.get_handler_suite(request.ws_resource) + if handler_suite is None: + return _default_passive_closing_handshake_handler(request) + return handler_suite.passive_closing_handshake(request) + + def get_handler_suite(self, resource): + """Retrieves two handlers (one for extra handshake processing, and one + for data transfer) for the given request as a HandlerSuite object. + """ + + fragment = None + if '#' in resource: + resource, fragment = resource.split('#', 1) + if '?' in resource: + resource = resource.split('?', 1)[0] + handler_suite = self._handler_suite_map.get(resource) + if handler_suite and fragment: + raise DispatchException('Fragment identifiers MUST NOT be used on ' + 'WebSocket URIs', + common.HTTP_STATUS_BAD_REQUEST) + return handler_suite + + def _source_handler_files_in_dir( + self, root_dir, scan_dir, allow_handlers_outside_root_dir): + """Source all the handler source files in the scan_dir directory. + + The resource path is determined relative to root_dir. + """ + + # We build a map from resource to handler code assuming that there's + # only one path from root_dir to scan_dir and it can be obtained by + # comparing realpath of them. + + # Here we cannot use abspath. See + # https://bugs.webkit.org/show_bug.cgi?id=31603 + + convert = _create_path_to_resource_converter(root_dir) + scan_realpath = os.path.realpath(scan_dir) + root_realpath = os.path.realpath(root_dir) + for path in _enumerate_handler_file_paths(scan_realpath): + if (not allow_handlers_outside_root_dir and + (not os.path.realpath(path).startswith(root_realpath))): + self._logger.debug( + 'Canonical path of %s is not under root directory' % + path) + continue + try: + handler_suite = _source_handler_file(open(path).read()) + except DispatchException, e: + self._source_warnings.append('%s: %s' % (path, e)) + continue + resource = convert(path) + if resource is None: + self._logger.debug( + 'Path to resource conversion on %s failed' % path) + else: + self._handler_suite_map[convert(path)] = handler_suite + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/extensions.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/extensions.py new file mode 100644 index 000000000..49a9fdcf9 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/extensions.py @@ -0,0 +1,885 @@ +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +from mod_pywebsocket import common +from mod_pywebsocket import util +from mod_pywebsocket.http_header_util import quote_if_necessary + + +# The list of available server side extension processor classes. +_available_processors = {} +_compression_extension_names = [] + + +class ExtensionProcessorInterface(object): + + def __init__(self, request): + self._logger = util.get_class_logger(self) + + self._request = request + self._active = True + + def request(self): + return self._request + + def name(self): + return None + + def check_consistency_with_other_processors(self, processors): + pass + + def set_active(self, active): + self._active = active + + def is_active(self): + return self._active + + def _get_extension_response_internal(self): + return None + + def get_extension_response(self): + if not self._active: + self._logger.debug('Extension %s is deactivated', self.name()) + return None + + response = self._get_extension_response_internal() + if response is None: + self._active = False + return response + + def _setup_stream_options_internal(self, stream_options): + pass + + def setup_stream_options(self, stream_options): + if self._active: + self._setup_stream_options_internal(stream_options) + + +def _log_outgoing_compression_ratio( + logger, original_bytes, filtered_bytes, average_ratio): + # Print inf when ratio is not available. + ratio = float('inf') + if original_bytes != 0: + ratio = float(filtered_bytes) / original_bytes + + logger.debug('Outgoing compression ratio: %f (average: %f)' % + (ratio, average_ratio)) + + +def _log_incoming_compression_ratio( + logger, received_bytes, filtered_bytes, average_ratio): + # Print inf when ratio is not available. + ratio = float('inf') + if filtered_bytes != 0: + ratio = float(received_bytes) / filtered_bytes + + logger.debug('Incoming compression ratio: %f (average: %f)' % + (ratio, average_ratio)) + + +def _parse_window_bits(bits): + """Return parsed integer value iff the given string conforms to the + grammar of the window bits extension parameters. + """ + + if bits is None: + raise ValueError('Value is required') + + # For non integer values such as "10.0", ValueError will be raised. + int_bits = int(bits) + + # First condition is to drop leading zero case e.g. "08". + if bits != str(int_bits) or int_bits < 8 or int_bits > 15: + raise ValueError('Invalid value: %r' % bits) + + return int_bits + + +class _AverageRatioCalculator(object): + """Stores total bytes of original and result data, and calculates average + result / original ratio. + """ + + def __init__(self): + self._total_original_bytes = 0 + self._total_result_bytes = 0 + + def add_original_bytes(self, value): + self._total_original_bytes += value + + def add_result_bytes(self, value): + self._total_result_bytes += value + + def get_average_ratio(self): + if self._total_original_bytes != 0: + return (float(self._total_result_bytes) / + self._total_original_bytes) + else: + return float('inf') + + +class DeflateFrameExtensionProcessor(ExtensionProcessorInterface): + """deflate-frame extension processor. + + Specification: + http://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate + """ + + _WINDOW_BITS_PARAM = 'max_window_bits' + _NO_CONTEXT_TAKEOVER_PARAM = 'no_context_takeover' + + def __init__(self, request): + ExtensionProcessorInterface.__init__(self, request) + self._logger = util.get_class_logger(self) + + self._response_window_bits = None + self._response_no_context_takeover = False + self._bfinal = False + + # Calculates + # (Total outgoing bytes supplied to this filter) / + # (Total bytes sent to the network after applying this filter) + self._outgoing_average_ratio_calculator = _AverageRatioCalculator() + + # Calculates + # (Total bytes received from the network) / + # (Total incoming bytes obtained after applying this filter) + self._incoming_average_ratio_calculator = _AverageRatioCalculator() + + def name(self): + return common.DEFLATE_FRAME_EXTENSION + + def _get_extension_response_internal(self): + # Any unknown parameter will be just ignored. + + window_bits = None + if self._request.has_parameter(self._WINDOW_BITS_PARAM): + window_bits = self._request.get_parameter_value( + self._WINDOW_BITS_PARAM) + try: + window_bits = _parse_window_bits(window_bits) + except ValueError, e: + return None + + no_context_takeover = self._request.has_parameter( + self._NO_CONTEXT_TAKEOVER_PARAM) + if (no_context_takeover and + self._request.get_parameter_value( + self._NO_CONTEXT_TAKEOVER_PARAM) is not None): + return None + + self._rfc1979_deflater = util._RFC1979Deflater( + window_bits, no_context_takeover) + + self._rfc1979_inflater = util._RFC1979Inflater() + + self._compress_outgoing = True + + response = common.ExtensionParameter(self._request.name()) + + if self._response_window_bits is not None: + response.add_parameter( + self._WINDOW_BITS_PARAM, str(self._response_window_bits)) + if self._response_no_context_takeover: + response.add_parameter( + self._NO_CONTEXT_TAKEOVER_PARAM, None) + + self._logger.debug( + 'Enable %s extension (' + 'request: window_bits=%s; no_context_takeover=%r, ' + 'response: window_wbits=%s; no_context_takeover=%r)' % + (self._request.name(), + window_bits, + no_context_takeover, + self._response_window_bits, + self._response_no_context_takeover)) + + return response + + def _setup_stream_options_internal(self, stream_options): + + class _OutgoingFilter(object): + + def __init__(self, parent): + self._parent = parent + + def filter(self, frame): + self._parent._outgoing_filter(frame) + + class _IncomingFilter(object): + + def __init__(self, parent): + self._parent = parent + + def filter(self, frame): + self._parent._incoming_filter(frame) + + stream_options.outgoing_frame_filters.append( + _OutgoingFilter(self)) + stream_options.incoming_frame_filters.insert( + 0, _IncomingFilter(self)) + + def set_response_window_bits(self, value): + self._response_window_bits = value + + def set_response_no_context_takeover(self, value): + self._response_no_context_takeover = value + + def set_bfinal(self, value): + self._bfinal = value + + def enable_outgoing_compression(self): + self._compress_outgoing = True + + def disable_outgoing_compression(self): + self._compress_outgoing = False + + def _outgoing_filter(self, frame): + """Transform outgoing frames. This method is called only by + an _OutgoingFilter instance. + """ + + original_payload_size = len(frame.payload) + self._outgoing_average_ratio_calculator.add_original_bytes( + original_payload_size) + + if (not self._compress_outgoing or + common.is_control_opcode(frame.opcode)): + self._outgoing_average_ratio_calculator.add_result_bytes( + original_payload_size) + return + + frame.payload = self._rfc1979_deflater.filter( + frame.payload, bfinal=self._bfinal) + frame.rsv1 = 1 + + filtered_payload_size = len(frame.payload) + self._outgoing_average_ratio_calculator.add_result_bytes( + filtered_payload_size) + + _log_outgoing_compression_ratio( + self._logger, + original_payload_size, + filtered_payload_size, + self._outgoing_average_ratio_calculator.get_average_ratio()) + + def _incoming_filter(self, frame): + """Transform incoming frames. This method is called only by + an _IncomingFilter instance. + """ + + received_payload_size = len(frame.payload) + self._incoming_average_ratio_calculator.add_result_bytes( + received_payload_size) + + if frame.rsv1 != 1 or common.is_control_opcode(frame.opcode): + self._incoming_average_ratio_calculator.add_original_bytes( + received_payload_size) + return + + frame.payload = self._rfc1979_inflater.filter(frame.payload) + frame.rsv1 = 0 + + filtered_payload_size = len(frame.payload) + self._incoming_average_ratio_calculator.add_original_bytes( + filtered_payload_size) + + _log_incoming_compression_ratio( + self._logger, + received_payload_size, + filtered_payload_size, + self._incoming_average_ratio_calculator.get_average_ratio()) + + +_available_processors[common.DEFLATE_FRAME_EXTENSION] = ( + DeflateFrameExtensionProcessor) +_compression_extension_names.append(common.DEFLATE_FRAME_EXTENSION) + +_available_processors[common.X_WEBKIT_DEFLATE_FRAME_EXTENSION] = ( + DeflateFrameExtensionProcessor) +_compression_extension_names.append(common.X_WEBKIT_DEFLATE_FRAME_EXTENSION) + + +def _parse_compression_method(data): + """Parses the value of "method" extension parameter.""" + + return common.parse_extensions(data) + + +def _create_accepted_method_desc(method_name, method_params): + """Creates accepted-method-desc from given method name and parameters""" + + extension = common.ExtensionParameter(method_name) + for name, value in method_params: + extension.add_parameter(name, value) + return common.format_extension(extension) + + +class CompressionExtensionProcessorBase(ExtensionProcessorInterface): + """Base class for perframe-compress and permessage-compress extension.""" + + _METHOD_PARAM = 'method' + + def __init__(self, request): + ExtensionProcessorInterface.__init__(self, request) + self._logger = util.get_class_logger(self) + self._compression_method_name = None + self._compression_processor = None + self._compression_processor_hook = None + + def name(self): + return '' + + def _lookup_compression_processor(self, method_desc): + return None + + def _get_compression_processor_response(self): + """Looks up the compression processor based on the self._request and + returns the compression processor's response. + """ + + method_list = self._request.get_parameter_value(self._METHOD_PARAM) + if method_list is None: + return None + methods = _parse_compression_method(method_list) + if methods is None: + return None + comression_processor = None + # The current implementation tries only the first method that matches + # supported algorithm. Following methods aren't tried even if the + # first one is rejected. + # TODO(bashi): Need to clarify this behavior. + for method_desc in methods: + compression_processor = self._lookup_compression_processor( + method_desc) + if compression_processor is not None: + self._compression_method_name = method_desc.name() + break + if compression_processor is None: + return None + + if self._compression_processor_hook: + self._compression_processor_hook(compression_processor) + + processor_response = compression_processor.get_extension_response() + if processor_response is None: + return None + self._compression_processor = compression_processor + return processor_response + + def _get_extension_response_internal(self): + processor_response = self._get_compression_processor_response() + if processor_response is None: + return None + + response = common.ExtensionParameter(self._request.name()) + accepted_method_desc = _create_accepted_method_desc( + self._compression_method_name, + processor_response.get_parameters()) + response.add_parameter(self._METHOD_PARAM, accepted_method_desc) + self._logger.debug( + 'Enable %s extension (method: %s)' % + (self._request.name(), self._compression_method_name)) + return response + + def _setup_stream_options_internal(self, stream_options): + if self._compression_processor is None: + return + self._compression_processor.setup_stream_options(stream_options) + + def set_compression_processor_hook(self, hook): + self._compression_processor_hook = hook + + def get_compression_processor(self): + return self._compression_processor + + +class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface): + """permessage-deflate extension processor. It's also used for + permessage-compress extension when the deflate method is chosen. + + Specification: + http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-08 + """ + + _SERVER_MAX_WINDOW_BITS_PARAM = 'server_max_window_bits' + _SERVER_NO_CONTEXT_TAKEOVER_PARAM = 'server_no_context_takeover' + _CLIENT_MAX_WINDOW_BITS_PARAM = 'client_max_window_bits' + _CLIENT_NO_CONTEXT_TAKEOVER_PARAM = 'client_no_context_takeover' + + def __init__(self, request, draft08=True): + """Construct PerMessageDeflateExtensionProcessor + + Args: + draft08: Follow the constraints on the parameters that were not + specified for permessage-compress but are specified for + permessage-deflate as on + draft-ietf-hybi-permessage-compression-08. + """ + + ExtensionProcessorInterface.__init__(self, request) + self._logger = util.get_class_logger(self) + + self._preferred_client_max_window_bits = None + self._client_no_context_takeover = False + + self._draft08 = draft08 + + def name(self): + return 'deflate' + + def _get_extension_response_internal(self): + if self._draft08: + for name in self._request.get_parameter_names(): + if name not in [self._SERVER_MAX_WINDOW_BITS_PARAM, + self._SERVER_NO_CONTEXT_TAKEOVER_PARAM, + self._CLIENT_MAX_WINDOW_BITS_PARAM]: + self._logger.debug('Unknown parameter: %r', name) + return None + else: + # Any unknown parameter will be just ignored. + pass + + server_max_window_bits = None + if self._request.has_parameter(self._SERVER_MAX_WINDOW_BITS_PARAM): + server_max_window_bits = self._request.get_parameter_value( + self._SERVER_MAX_WINDOW_BITS_PARAM) + try: + server_max_window_bits = _parse_window_bits( + server_max_window_bits) + except ValueError, e: + self._logger.debug('Bad %s parameter: %r', + self._SERVER_MAX_WINDOW_BITS_PARAM, + e) + return None + + server_no_context_takeover = self._request.has_parameter( + self._SERVER_NO_CONTEXT_TAKEOVER_PARAM) + if (server_no_context_takeover and + self._request.get_parameter_value( + self._SERVER_NO_CONTEXT_TAKEOVER_PARAM) is not None): + self._logger.debug('%s parameter must not have a value: %r', + self._SERVER_NO_CONTEXT_TAKEOVER_PARAM, + server_no_context_takeover) + return None + + # client_max_window_bits from a client indicates whether the client can + # accept client_max_window_bits from a server or not. + client_client_max_window_bits = self._request.has_parameter( + self._CLIENT_MAX_WINDOW_BITS_PARAM) + if (self._draft08 and + client_client_max_window_bits and + self._request.get_parameter_value( + self._CLIENT_MAX_WINDOW_BITS_PARAM) is not None): + self._logger.debug('%s parameter must not have a value in a ' + 'client\'s opening handshake: %r', + self._CLIENT_MAX_WINDOW_BITS_PARAM, + client_client_max_window_bits) + return None + + self._rfc1979_deflater = util._RFC1979Deflater( + server_max_window_bits, server_no_context_takeover) + + # Note that we prepare for incoming messages compressed with window + # bits upto 15 regardless of the client_max_window_bits value to be + # sent to the client. + self._rfc1979_inflater = util._RFC1979Inflater() + + self._framer = _PerMessageDeflateFramer( + server_max_window_bits, server_no_context_takeover) + self._framer.set_bfinal(False) + self._framer.set_compress_outgoing_enabled(True) + + response = common.ExtensionParameter(self._request.name()) + + if server_max_window_bits is not None: + response.add_parameter( + self._SERVER_MAX_WINDOW_BITS_PARAM, + str(server_max_window_bits)) + + if server_no_context_takeover: + response.add_parameter( + self._SERVER_NO_CONTEXT_TAKEOVER_PARAM, None) + + if self._preferred_client_max_window_bits is not None: + if self._draft08 and not client_client_max_window_bits: + self._logger.debug('Processor is configured to use %s but ' + 'the client cannot accept it', + self._CLIENT_MAX_WINDOW_BITS_PARAM) + return None + response.add_parameter( + self._CLIENT_MAX_WINDOW_BITS_PARAM, + str(self._preferred_client_max_window_bits)) + + if self._client_no_context_takeover: + response.add_parameter( + self._CLIENT_NO_CONTEXT_TAKEOVER_PARAM, None) + + self._logger.debug( + 'Enable %s extension (' + 'request: server_max_window_bits=%s; ' + 'server_no_context_takeover=%r, ' + 'response: client_max_window_bits=%s; ' + 'client_no_context_takeover=%r)' % + (self._request.name(), + server_max_window_bits, + server_no_context_takeover, + self._preferred_client_max_window_bits, + self._client_no_context_takeover)) + + return response + + def _setup_stream_options_internal(self, stream_options): + self._framer.setup_stream_options(stream_options) + + def set_client_max_window_bits(self, value): + """If this option is specified, this class adds the + client_max_window_bits extension parameter to the handshake response, + but doesn't reduce the LZ77 sliding window size of its inflater. + I.e., you can use this for testing client implementation but cannot + reduce memory usage of this class. + + If this method has been called with True and an offer without the + client_max_window_bits extension parameter is received, + - (When processing the permessage-deflate extension) this processor + declines the request. + - (When processing the permessage-compress extension) this processor + accepts the request. + """ + + self._preferred_client_max_window_bits = value + + def set_client_no_context_takeover(self, value): + """If this option is specified, this class adds the + client_no_context_takeover extension parameter to the handshake + response, but doesn't reset inflater for each message. I.e., you can + use this for testing client implementation but cannot reduce memory + usage of this class. + """ + + self._client_no_context_takeover = value + + def set_bfinal(self, value): + self._framer.set_bfinal(value) + + def enable_outgoing_compression(self): + self._framer.set_compress_outgoing_enabled(True) + + def disable_outgoing_compression(self): + self._framer.set_compress_outgoing_enabled(False) + + +class _PerMessageDeflateFramer(object): + """A framer for extensions with per-message DEFLATE feature.""" + + def __init__(self, deflate_max_window_bits, deflate_no_context_takeover): + self._logger = util.get_class_logger(self) + + self._rfc1979_deflater = util._RFC1979Deflater( + deflate_max_window_bits, deflate_no_context_takeover) + + self._rfc1979_inflater = util._RFC1979Inflater() + + self._bfinal = False + + self._compress_outgoing_enabled = False + + # True if a message is fragmented and compression is ongoing. + self._compress_ongoing = False + + # Calculates + # (Total outgoing bytes supplied to this filter) / + # (Total bytes sent to the network after applying this filter) + self._outgoing_average_ratio_calculator = _AverageRatioCalculator() + + # Calculates + # (Total bytes received from the network) / + # (Total incoming bytes obtained after applying this filter) + self._incoming_average_ratio_calculator = _AverageRatioCalculator() + + def set_bfinal(self, value): + self._bfinal = value + + def set_compress_outgoing_enabled(self, value): + self._compress_outgoing_enabled = value + + def _process_incoming_message(self, message, decompress): + if not decompress: + return message + + received_payload_size = len(message) + self._incoming_average_ratio_calculator.add_result_bytes( + received_payload_size) + + message = self._rfc1979_inflater.filter(message) + + filtered_payload_size = len(message) + self._incoming_average_ratio_calculator.add_original_bytes( + filtered_payload_size) + + _log_incoming_compression_ratio( + self._logger, + received_payload_size, + filtered_payload_size, + self._incoming_average_ratio_calculator.get_average_ratio()) + + return message + + def _process_outgoing_message(self, message, end, binary): + if not binary: + message = message.encode('utf-8') + + if not self._compress_outgoing_enabled: + return message + + original_payload_size = len(message) + self._outgoing_average_ratio_calculator.add_original_bytes( + original_payload_size) + + message = self._rfc1979_deflater.filter( + message, end=end, bfinal=self._bfinal) + + filtered_payload_size = len(message) + self._outgoing_average_ratio_calculator.add_result_bytes( + filtered_payload_size) + + _log_outgoing_compression_ratio( + self._logger, + original_payload_size, + filtered_payload_size, + self._outgoing_average_ratio_calculator.get_average_ratio()) + + if not self._compress_ongoing: + self._outgoing_frame_filter.set_compression_bit() + self._compress_ongoing = not end + return message + + def _process_incoming_frame(self, frame): + if frame.rsv1 == 1 and not common.is_control_opcode(frame.opcode): + self._incoming_message_filter.decompress_next_message() + frame.rsv1 = 0 + + def _process_outgoing_frame(self, frame, compression_bit): + if (not compression_bit or + common.is_control_opcode(frame.opcode)): + return + + frame.rsv1 = 1 + + def setup_stream_options(self, stream_options): + """Creates filters and sets them to the StreamOptions.""" + + class _OutgoingMessageFilter(object): + + def __init__(self, parent): + self._parent = parent + + def filter(self, message, end=True, binary=False): + return self._parent._process_outgoing_message( + message, end, binary) + + class _IncomingMessageFilter(object): + + def __init__(self, parent): + self._parent = parent + self._decompress_next_message = False + + def decompress_next_message(self): + self._decompress_next_message = True + + def filter(self, message): + message = self._parent._process_incoming_message( + message, self._decompress_next_message) + self._decompress_next_message = False + return message + + self._outgoing_message_filter = _OutgoingMessageFilter(self) + self._incoming_message_filter = _IncomingMessageFilter(self) + stream_options.outgoing_message_filters.append( + self._outgoing_message_filter) + stream_options.incoming_message_filters.append( + self._incoming_message_filter) + + class _OutgoingFrameFilter(object): + + def __init__(self, parent): + self._parent = parent + self._set_compression_bit = False + + def set_compression_bit(self): + self._set_compression_bit = True + + def filter(self, frame): + self._parent._process_outgoing_frame( + frame, self._set_compression_bit) + self._set_compression_bit = False + + class _IncomingFrameFilter(object): + + def __init__(self, parent): + self._parent = parent + + def filter(self, frame): + self._parent._process_incoming_frame(frame) + + self._outgoing_frame_filter = _OutgoingFrameFilter(self) + self._incoming_frame_filter = _IncomingFrameFilter(self) + stream_options.outgoing_frame_filters.append( + self._outgoing_frame_filter) + stream_options.incoming_frame_filters.append( + self._incoming_frame_filter) + + stream_options.encode_text_message_to_utf8 = False + + +_available_processors[common.PERMESSAGE_DEFLATE_EXTENSION] = ( + PerMessageDeflateExtensionProcessor) +# TODO(tyoshino): Reorganize class names. +_compression_extension_names.append('deflate') + + +class PerMessageCompressExtensionProcessor( + CompressionExtensionProcessorBase): + """permessage-compress extension processor. + + Specification: + http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression + """ + + _DEFLATE_METHOD = 'deflate' + + def __init__(self, request): + CompressionExtensionProcessorBase.__init__(self, request) + + def name(self): + return common.PERMESSAGE_COMPRESSION_EXTENSION + + def _lookup_compression_processor(self, method_desc): + if method_desc.name() == self._DEFLATE_METHOD: + return PerMessageDeflateExtensionProcessor(method_desc, False) + return None + + +_available_processors[common.PERMESSAGE_COMPRESSION_EXTENSION] = ( + PerMessageCompressExtensionProcessor) +_compression_extension_names.append(common.PERMESSAGE_COMPRESSION_EXTENSION) + + +class MuxExtensionProcessor(ExtensionProcessorInterface): + """WebSocket multiplexing extension processor.""" + + _QUOTA_PARAM = 'quota' + + def __init__(self, request): + ExtensionProcessorInterface.__init__(self, request) + self._quota = 0 + self._extensions = [] + + def name(self): + return common.MUX_EXTENSION + + def check_consistency_with_other_processors(self, processors): + before_mux = True + for processor in processors: + name = processor.name() + if name == self.name(): + before_mux = False + continue + if not processor.is_active(): + continue + if before_mux: + # Mux extension cannot be used after extensions + # that depend on frame boundary, extension data field, or any + # reserved bits which are attributed to each frame. + if (name == common.DEFLATE_FRAME_EXTENSION or + name == common.X_WEBKIT_DEFLATE_FRAME_EXTENSION): + self.set_active(False) + return + else: + # Mux extension should not be applied before any history-based + # compression extension. + if (name == common.DEFLATE_FRAME_EXTENSION or + name == common.X_WEBKIT_DEFLATE_FRAME_EXTENSION or + name == common.PERMESSAGE_COMPRESSION_EXTENSION or + name == common.X_WEBKIT_PERMESSAGE_COMPRESSION_EXTENSION): + self.set_active(False) + return + + def _get_extension_response_internal(self): + self._active = False + quota = self._request.get_parameter_value(self._QUOTA_PARAM) + if quota is not None: + try: + quota = int(quota) + except ValueError, e: + return None + if quota < 0 or quota >= 2 ** 32: + return None + self._quota = quota + + self._active = True + return common.ExtensionParameter(common.MUX_EXTENSION) + + def _setup_stream_options_internal(self, stream_options): + pass + + def set_quota(self, quota): + self._quota = quota + + def quota(self): + return self._quota + + def set_extensions(self, extensions): + self._extensions = extensions + + def extensions(self): + return self._extensions + + +_available_processors[common.MUX_EXTENSION] = MuxExtensionProcessor + + +def get_extension_processor(extension_request): + """Given an ExtensionParameter representing an extension offer received + from a client, configures and returns an instance of the corresponding + extension processor class. + """ + + processor_class = _available_processors.get(extension_request.name()) + if processor_class is None: + return None + return processor_class(extension_request) + + +def is_compression_extension(extension_name): + return extension_name in _compression_extension_names + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/fast_masking.i b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/fast_masking.i new file mode 100644 index 000000000..ddaad27f5 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/fast_masking.i @@ -0,0 +1,98 @@ +// Copyright 2013, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +%module fast_masking + +%include "cstring.i" + +%{ +#include <cstring> + +#ifdef __SSE2__ +#include <emmintrin.h> +#endif +%} + +%apply (char *STRING, int LENGTH) { + (const char* payload, int payload_length), + (const char* masking_key, int masking_key_length) }; +%cstring_output_allocate_size( + char** result, int* result_length, delete [] *$1); + +%inline %{ + +void mask( + const char* payload, int payload_length, + const char* masking_key, int masking_key_length, + int masking_key_index, + char** result, int* result_length) { + *result = new char[payload_length]; + *result_length = payload_length; + memcpy(*result, payload, payload_length); + + char* cursor = *result; + char* cursor_end = *result + *result_length; + +#ifdef __SSE2__ + while ((cursor < cursor_end) && + (reinterpret_cast<size_t>(cursor) & 0xf)) { + *cursor ^= masking_key[masking_key_index]; + ++cursor; + masking_key_index = (masking_key_index + 1) % masking_key_length; + } + if (cursor == cursor_end) { + return; + } + + const int kBlockSize = 16; + __m128i masking_key_block; + for (int i = 0; i < kBlockSize; ++i) { + *(reinterpret_cast<char*>(&masking_key_block) + i) = + masking_key[masking_key_index]; + masking_key_index = (masking_key_index + 1) % masking_key_length; + } + + while (cursor + kBlockSize <= cursor_end) { + __m128i payload_block = + _mm_load_si128(reinterpret_cast<__m128i*>(cursor)); + _mm_stream_si128(reinterpret_cast<__m128i*>(cursor), + _mm_xor_si128(payload_block, masking_key_block)); + cursor += kBlockSize; + } +#endif + + while (cursor < cursor_end) { + *cursor ^= masking_key[masking_key_index]; + ++cursor; + masking_key_index = (masking_key_index + 1) % masking_key_length; + } +} + +%} diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/__init__.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/__init__.py new file mode 100644 index 000000000..194f6b395 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/__init__.py @@ -0,0 +1,110 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""WebSocket opening handshake processor. This class try to apply available +opening handshake processors for each protocol version until a connection is +successfully established. +""" + + +import logging + +from mod_pywebsocket import common +from mod_pywebsocket.handshake import hybi00 +from mod_pywebsocket.handshake import hybi +# Export AbortedByUserException, HandshakeException, and VersionException +# symbol from this module. +from mod_pywebsocket.handshake._base import AbortedByUserException +from mod_pywebsocket.handshake._base import HandshakeException +from mod_pywebsocket.handshake._base import VersionException + + +_LOGGER = logging.getLogger(__name__) + + +def do_handshake(request, dispatcher, allowDraft75=False, strict=False): + """Performs WebSocket handshake. + + Args: + request: mod_python request. + dispatcher: Dispatcher (dispatch.Dispatcher). + allowDraft75: obsolete argument. ignored. + strict: obsolete argument. ignored. + + Handshaker will add attributes such as ws_resource in performing + handshake. + """ + + _LOGGER.debug('Client\'s opening handshake resource: %r', request.uri) + # To print mimetools.Message as escaped one-line string, we converts + # headers_in to dict object. Without conversion, if we use %r, it just + # prints the type and address, and if we use %s, it prints the original + # header string as multiple lines. + # + # Both mimetools.Message and MpTable_Type of mod_python can be + # converted to dict. + # + # mimetools.Message.__str__ returns the original header string. + # dict(mimetools.Message object) returns the map from header names to + # header values. While MpTable_Type doesn't have such __str__ but just + # __repr__ which formats itself as well as dictionary object. + _LOGGER.debug( + 'Client\'s opening handshake headers: %r', dict(request.headers_in)) + + handshakers = [] + handshakers.append( + ('RFC 6455', hybi.Handshaker(request, dispatcher))) + handshakers.append( + ('HyBi 00', hybi00.Handshaker(request, dispatcher))) + + for name, handshaker in handshakers: + _LOGGER.debug('Trying protocol version %s', name) + try: + handshaker.do_handshake() + _LOGGER.info('Established (%s protocol)', name) + return + except HandshakeException, e: + _LOGGER.debug( + 'Failed to complete opening handshake as %s protocol: %r', + name, e) + if e.status: + raise e + except AbortedByUserException, e: + raise + except VersionException, e: + raise + + # TODO(toyoshim): Add a test to cover the case all handshakers fail. + raise HandshakeException( + 'Failed to complete opening handshake for all available protocols', + status=common.HTTP_STATUS_BAD_REQUEST) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/_base.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/_base.py new file mode 100644 index 000000000..c993a584b --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/_base.py @@ -0,0 +1,182 @@ +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Common functions and exceptions used by WebSocket opening handshake +processors. +""" + + +from mod_pywebsocket import common +from mod_pywebsocket import http_header_util + + +class AbortedByUserException(Exception): + """Exception for aborting a connection intentionally. + + If this exception is raised in do_extra_handshake handler, the connection + will be abandoned. No other WebSocket or HTTP(S) handler will be invoked. + + If this exception is raised in transfer_data_handler, the connection will + be closed without closing handshake. No other WebSocket or HTTP(S) handler + will be invoked. + """ + + pass + + +class HandshakeException(Exception): + """This exception will be raised when an error occurred while processing + WebSocket initial handshake. + """ + + def __init__(self, name, status=None): + super(HandshakeException, self).__init__(name) + self.status = status + + +class VersionException(Exception): + """This exception will be raised when a version of client request does not + match with version the server supports. + """ + + def __init__(self, name, supported_versions=''): + """Construct an instance. + + Args: + supported_version: a str object to show supported hybi versions. + (e.g. '8, 13') + """ + super(VersionException, self).__init__(name) + self.supported_versions = supported_versions + + +def get_default_port(is_secure): + if is_secure: + return common.DEFAULT_WEB_SOCKET_SECURE_PORT + else: + return common.DEFAULT_WEB_SOCKET_PORT + + +def validate_subprotocol(subprotocol): + """Validate a value in the Sec-WebSocket-Protocol field. + + See the Section 4.1., 4.2.2., and 4.3. of RFC 6455. + """ + + if not subprotocol: + raise HandshakeException('Invalid subprotocol name: empty') + + # Parameter should be encoded HTTP token. + state = http_header_util.ParsingState(subprotocol) + token = http_header_util.consume_token(state) + rest = http_header_util.peek(state) + # If |rest| is not None, |subprotocol| is not one token or invalid. If + # |rest| is None, |token| must not be None because |subprotocol| is + # concatenation of |token| and |rest| and is not None. + if rest is not None: + raise HandshakeException('Invalid non-token string in subprotocol ' + 'name: %r' % rest) + + +def parse_host_header(request): + fields = request.headers_in[common.HOST_HEADER].split(':', 1) + if len(fields) == 1: + return fields[0], get_default_port(request.is_https()) + try: + return fields[0], int(fields[1]) + except ValueError, e: + raise HandshakeException('Invalid port number format: %r' % e) + + +def format_header(name, value): + return '%s: %s\r\n' % (name, value) + + +def get_mandatory_header(request, key): + value = request.headers_in.get(key) + if value is None: + raise HandshakeException('Header %s is not defined' % key) + return value + + +def validate_mandatory_header(request, key, expected_value, fail_status=None): + value = get_mandatory_header(request, key) + + if value.lower() != expected_value.lower(): + raise HandshakeException( + 'Expected %r for header %s but found %r (case-insensitive)' % + (expected_value, key, value), status=fail_status) + + +def check_request_line(request): + # 5.1 1. The three character UTF-8 string "GET". + # 5.1 2. A UTF-8-encoded U+0020 SPACE character (0x20 byte). + if request.method != 'GET': + raise HandshakeException('Method is not GET: %r' % request.method) + + if request.protocol != 'HTTP/1.1': + raise HandshakeException('Version is not HTTP/1.1: %r' % + request.protocol) + + +def parse_token_list(data): + """Parses a header value which follows 1#token and returns parsed elements + as a list of strings. + + Leading LWSes must be trimmed. + """ + + state = http_header_util.ParsingState(data) + + token_list = [] + + while True: + token = http_header_util.consume_token(state) + if token is not None: + token_list.append(token) + + http_header_util.consume_lwses(state) + + if http_header_util.peek(state) is None: + break + + if not http_header_util.consume_string(state, ','): + raise HandshakeException( + 'Expected a comma but found %r' % http_header_util.peek(state)) + + http_header_util.consume_lwses(state) + + if len(token_list) == 0: + raise HandshakeException('No valid token found') + + return token_list + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/hybi.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/hybi.py new file mode 100644 index 000000000..1ad10ea37 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/hybi.py @@ -0,0 +1,420 @@ +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""This file provides the opening handshake processor for the WebSocket +protocol (RFC 6455). + +Specification: +http://tools.ietf.org/html/rfc6455 +""" + + +# Note: request.connection.write is used in this module, even though mod_python +# document says that it should be used only in connection handlers. +# Unfortunately, we have no other options. For example, request.write is not +# suitable because it doesn't allow direct raw bytes writing. + + +import base64 +import logging +import os +import re + +from mod_pywebsocket import common +from mod_pywebsocket.extensions import get_extension_processor +from mod_pywebsocket.extensions import is_compression_extension +from mod_pywebsocket.handshake._base import check_request_line +from mod_pywebsocket.handshake._base import format_header +from mod_pywebsocket.handshake._base import get_mandatory_header +from mod_pywebsocket.handshake._base import HandshakeException +from mod_pywebsocket.handshake._base import parse_token_list +from mod_pywebsocket.handshake._base import validate_mandatory_header +from mod_pywebsocket.handshake._base import validate_subprotocol +from mod_pywebsocket.handshake._base import VersionException +from mod_pywebsocket.stream import Stream +from mod_pywebsocket.stream import StreamOptions +from mod_pywebsocket import util + + +# Used to validate the value in the Sec-WebSocket-Key header strictly. RFC 4648 +# disallows non-zero padding, so the character right before == must be any of +# A, Q, g and w. +_SEC_WEBSOCKET_KEY_REGEX = re.compile('^[+/0-9A-Za-z]{21}[AQgw]==$') + +# Defining aliases for values used frequently. +_VERSION_LATEST = common.VERSION_HYBI_LATEST +_VERSION_LATEST_STRING = str(_VERSION_LATEST) +_SUPPORTED_VERSIONS = [ + _VERSION_LATEST, +] + + +def compute_accept(key): + """Computes value for the Sec-WebSocket-Accept header from value of the + Sec-WebSocket-Key header. + """ + + accept_binary = util.sha1_hash( + key + common.WEBSOCKET_ACCEPT_UUID).digest() + accept = base64.b64encode(accept_binary) + + return (accept, accept_binary) + + +class Handshaker(object): + """Opening handshake processor for the WebSocket protocol (RFC 6455).""" + + def __init__(self, request, dispatcher): + """Construct an instance. + + Args: + request: mod_python request. + dispatcher: Dispatcher (dispatch.Dispatcher). + + Handshaker will add attributes such as ws_resource during handshake. + """ + + self._logger = util.get_class_logger(self) + + self._request = request + self._dispatcher = dispatcher + + def _validate_connection_header(self): + connection = get_mandatory_header( + self._request, common.CONNECTION_HEADER) + + try: + connection_tokens = parse_token_list(connection) + except HandshakeException, e: + raise HandshakeException( + 'Failed to parse %s: %s' % (common.CONNECTION_HEADER, e)) + + connection_is_valid = False + for token in connection_tokens: + if token.lower() == common.UPGRADE_CONNECTION_TYPE.lower(): + connection_is_valid = True + break + if not connection_is_valid: + raise HandshakeException( + '%s header doesn\'t contain "%s"' % + (common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE)) + + def do_handshake(self): + self._request.ws_close_code = None + self._request.ws_close_reason = None + + # Parsing. + + check_request_line(self._request) + + validate_mandatory_header( + self._request, + common.UPGRADE_HEADER, + common.WEBSOCKET_UPGRADE_TYPE) + + self._validate_connection_header() + + self._request.ws_resource = self._request.uri + + unused_host = get_mandatory_header(self._request, common.HOST_HEADER) + + self._request.ws_version = self._check_version() + + try: + self._get_origin() + self._set_protocol() + self._parse_extensions() + + # Key validation, response generation. + + key = self._get_key() + (accept, accept_binary) = compute_accept(key) + self._logger.debug( + '%s: %r (%s)', + common.SEC_WEBSOCKET_ACCEPT_HEADER, + accept, + util.hexify(accept_binary)) + + self._logger.debug('Protocol version is RFC 6455') + + # Setup extension processors. + + processors = [] + if self._request.ws_requested_extensions is not None: + for extension_request in self._request.ws_requested_extensions: + processor = get_extension_processor(extension_request) + # Unknown extension requests are just ignored. + if processor is not None: + processors.append(processor) + self._request.ws_extension_processors = processors + + # List of extra headers. The extra handshake handler may add header + # data as name/value pairs to this list and pywebsocket appends + # them to the WebSocket handshake. + self._request.extra_headers = [] + + # Extra handshake handler may modify/remove processors. + self._dispatcher.do_extra_handshake(self._request) + processors = filter(lambda processor: processor is not None, + self._request.ws_extension_processors) + + # Ask each processor if there are extensions on the request which + # cannot co-exist. When processor decided other processors cannot + # co-exist with it, the processor marks them (or itself) as + # "inactive". The first extension processor has the right to + # make the final call. + for processor in reversed(processors): + if processor.is_active(): + processor.check_consistency_with_other_processors( + processors) + processors = filter(lambda processor: processor.is_active(), + processors) + + accepted_extensions = [] + + # We need to take into account of mux extension here. + # If mux extension exists: + # - Remove processors of extensions for logical channel, + # which are processors located before the mux processor + # - Pass extension requests for logical channel to mux processor + # - Attach the mux processor to the request. It will be referred + # by dispatcher to see whether the dispatcher should use mux + # handler or not. + mux_index = -1 + for i, processor in enumerate(processors): + if processor.name() == common.MUX_EXTENSION: + mux_index = i + break + if mux_index >= 0: + logical_channel_extensions = [] + for processor in processors[:mux_index]: + logical_channel_extensions.append(processor.request()) + processor.set_active(False) + self._request.mux_processor = processors[mux_index] + self._request.mux_processor.set_extensions( + logical_channel_extensions) + processors = filter(lambda processor: processor.is_active(), + processors) + + stream_options = StreamOptions() + + for index, processor in enumerate(processors): + if not processor.is_active(): + continue + + extension_response = processor.get_extension_response() + if extension_response is None: + # Rejected. + continue + + accepted_extensions.append(extension_response) + + processor.setup_stream_options(stream_options) + + if not is_compression_extension(processor.name()): + continue + + # Inactivate all of the following compression extensions. + for j in xrange(index + 1, len(processors)): + if is_compression_extension(processors[j].name()): + processors[j].set_active(False) + + if len(accepted_extensions) > 0: + self._request.ws_extensions = accepted_extensions + self._logger.debug( + 'Extensions accepted: %r', + map(common.ExtensionParameter.name, accepted_extensions)) + else: + self._request.ws_extensions = None + + self._request.ws_stream = self._create_stream(stream_options) + + if self._request.ws_requested_protocols is not None: + if self._request.ws_protocol is None: + raise HandshakeException( + 'do_extra_handshake must choose one subprotocol from ' + 'ws_requested_protocols and set it to ws_protocol') + validate_subprotocol(self._request.ws_protocol) + + self._logger.debug( + 'Subprotocol accepted: %r', + self._request.ws_protocol) + else: + if self._request.ws_protocol is not None: + raise HandshakeException( + 'ws_protocol must be None when the client didn\'t ' + 'request any subprotocol') + + self._send_handshake(accept) + except HandshakeException, e: + if not e.status: + # Fallback to 400 bad request by default. + e.status = common.HTTP_STATUS_BAD_REQUEST + raise e + + def _get_origin(self): + origin_header = common.ORIGIN_HEADER + origin = self._request.headers_in.get(origin_header) + if origin is None: + self._logger.debug('Client request does not have origin header') + self._request.ws_origin = origin + + def _check_version(self): + version = get_mandatory_header(self._request, + common.SEC_WEBSOCKET_VERSION_HEADER) + if version == _VERSION_LATEST_STRING: + return _VERSION_LATEST + + if version.find(',') >= 0: + raise HandshakeException( + 'Multiple versions (%r) are not allowed for header %s' % + (version, common.SEC_WEBSOCKET_VERSION_HEADER), + status=common.HTTP_STATUS_BAD_REQUEST) + raise VersionException( + 'Unsupported version %r for header %s' % + (version, common.SEC_WEBSOCKET_VERSION_HEADER), + supported_versions=', '.join(map(str, _SUPPORTED_VERSIONS))) + + def _set_protocol(self): + self._request.ws_protocol = None + + protocol_header = self._request.headers_in.get( + common.SEC_WEBSOCKET_PROTOCOL_HEADER) + + if protocol_header is None: + self._request.ws_requested_protocols = None + return + + self._request.ws_requested_protocols = parse_token_list( + protocol_header) + self._logger.debug('Subprotocols requested: %r', + self._request.ws_requested_protocols) + + def _parse_extensions(self): + extensions_header = self._request.headers_in.get( + common.SEC_WEBSOCKET_EXTENSIONS_HEADER) + if not extensions_header: + self._request.ws_requested_extensions = None + return + + try: + self._request.ws_requested_extensions = common.parse_extensions( + extensions_header) + except common.ExtensionParsingException, e: + raise HandshakeException( + 'Failed to parse Sec-WebSocket-Extensions header: %r' % e) + + self._logger.debug( + 'Extensions requested: %r', + map(common.ExtensionParameter.name, + self._request.ws_requested_extensions)) + + def _validate_key(self, key): + if key.find(',') >= 0: + raise HandshakeException('Request has multiple %s header lines or ' + 'contains illegal character \',\': %r' % + (common.SEC_WEBSOCKET_KEY_HEADER, key)) + + # Validate + key_is_valid = False + try: + # Validate key by quick regex match before parsing by base64 + # module. Because base64 module skips invalid characters, we have + # to do this in advance to make this server strictly reject illegal + # keys. + if _SEC_WEBSOCKET_KEY_REGEX.match(key): + decoded_key = base64.b64decode(key) + if len(decoded_key) == 16: + key_is_valid = True + except TypeError, e: + pass + + if not key_is_valid: + raise HandshakeException( + 'Illegal value for header %s: %r' % + (common.SEC_WEBSOCKET_KEY_HEADER, key)) + + return decoded_key + + def _get_key(self): + key = get_mandatory_header( + self._request, common.SEC_WEBSOCKET_KEY_HEADER) + + decoded_key = self._validate_key(key) + + self._logger.debug( + '%s: %r (%s)', + common.SEC_WEBSOCKET_KEY_HEADER, + key, + util.hexify(decoded_key)) + + return key + + def _create_stream(self, stream_options): + return Stream(self._request, stream_options) + + def _create_handshake_response(self, accept): + response = [] + + response.append('HTTP/1.1 101 Switching Protocols\r\n') + + # WebSocket headers + response.append(format_header( + common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE)) + response.append(format_header( + common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE)) + response.append(format_header( + common.SEC_WEBSOCKET_ACCEPT_HEADER, accept)) + if self._request.ws_protocol is not None: + response.append(format_header( + common.SEC_WEBSOCKET_PROTOCOL_HEADER, + self._request.ws_protocol)) + if (self._request.ws_extensions is not None and + len(self._request.ws_extensions) != 0): + response.append(format_header( + common.SEC_WEBSOCKET_EXTENSIONS_HEADER, + common.format_extensions(self._request.ws_extensions))) + + # Headers not specific for WebSocket + for name, value in self._request.extra_headers: + response.append(format_header(name, value)) + + response.append('\r\n') + + return ''.join(response) + + def _send_handshake(self, accept): + raw_response = self._create_handshake_response(accept) + self._request.connection.write(raw_response) + self._logger.debug('Sent server\'s opening handshake: %r', + raw_response) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/hybi00.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/hybi00.py new file mode 100644 index 000000000..8757717a6 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/handshake/hybi00.py @@ -0,0 +1,293 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""This file provides the opening handshake processor for the WebSocket +protocol version HyBi 00. + +Specification: +http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00 +""" + + +# Note: request.connection.write/read are used in this module, even though +# mod_python document says that they should be used only in connection +# handlers. Unfortunately, we have no other options. For example, +# request.write/read are not suitable because they don't allow direct raw bytes +# writing/reading. + + +import logging +import re +import struct + +from mod_pywebsocket import common +from mod_pywebsocket.stream import StreamHixie75 +from mod_pywebsocket import util +from mod_pywebsocket.handshake._base import HandshakeException +from mod_pywebsocket.handshake._base import check_request_line +from mod_pywebsocket.handshake._base import format_header +from mod_pywebsocket.handshake._base import get_default_port +from mod_pywebsocket.handshake._base import get_mandatory_header +from mod_pywebsocket.handshake._base import parse_host_header +from mod_pywebsocket.handshake._base import validate_mandatory_header + + +_MANDATORY_HEADERS = [ + # key, expected value or None + [common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75], + [common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE], +] + + +def _validate_subprotocol(subprotocol): + """Checks if characters in subprotocol are in range between U+0020 and + U+007E. A value in the Sec-WebSocket-Protocol field need to satisfy this + requirement. + + See the Section 4.1. Opening handshake of the spec. + """ + + if not subprotocol: + raise HandshakeException('Invalid subprotocol name: empty') + + # Parameter should be in the range U+0020 to U+007E. + for c in subprotocol: + if not 0x20 <= ord(c) <= 0x7e: + raise HandshakeException( + 'Illegal character in subprotocol name: %r' % c) + + +def _check_header_lines(request, mandatory_headers): + check_request_line(request) + + # The expected field names, and the meaning of their corresponding + # values, are as follows. + # |Upgrade| and |Connection| + for key, expected_value in mandatory_headers: + validate_mandatory_header(request, key, expected_value) + + +def _build_location(request): + """Build WebSocket location for request.""" + + location_parts = [] + if request.is_https(): + location_parts.append(common.WEB_SOCKET_SECURE_SCHEME) + else: + location_parts.append(common.WEB_SOCKET_SCHEME) + location_parts.append('://') + host, port = parse_host_header(request) + connection_port = request.connection.local_addr[1] + if port != connection_port: + raise HandshakeException('Header/connection port mismatch: %d/%d' % + (port, connection_port)) + location_parts.append(host) + if (port != get_default_port(request.is_https())): + location_parts.append(':') + location_parts.append(str(port)) + location_parts.append(request.unparsed_uri) + return ''.join(location_parts) + + +class Handshaker(object): + """Opening handshake processor for the WebSocket protocol version HyBi 00. + """ + + def __init__(self, request, dispatcher): + """Construct an instance. + + Args: + request: mod_python request. + dispatcher: Dispatcher (dispatch.Dispatcher). + + Handshaker will add attributes such as ws_resource in performing + handshake. + """ + + self._logger = util.get_class_logger(self) + + self._request = request + self._dispatcher = dispatcher + + def do_handshake(self): + """Perform WebSocket Handshake. + + On _request, we set + ws_resource, ws_protocol, ws_location, ws_origin, ws_challenge, + ws_challenge_md5: WebSocket handshake information. + ws_stream: Frame generation/parsing class. + ws_version: Protocol version. + + Raises: + HandshakeException: when any error happened in parsing the opening + handshake request. + """ + + # 5.1 Reading the client's opening handshake. + # dispatcher sets it in self._request. + _check_header_lines(self._request, _MANDATORY_HEADERS) + self._set_resource() + self._set_subprotocol() + self._set_location() + self._set_origin() + self._set_challenge_response() + self._set_protocol_version() + + self._dispatcher.do_extra_handshake(self._request) + + self._send_handshake() + + def _set_resource(self): + self._request.ws_resource = self._request.uri + + def _set_subprotocol(self): + # |Sec-WebSocket-Protocol| + subprotocol = self._request.headers_in.get( + common.SEC_WEBSOCKET_PROTOCOL_HEADER) + if subprotocol is not None: + _validate_subprotocol(subprotocol) + self._request.ws_protocol = subprotocol + + def _set_location(self): + # |Host| + host = self._request.headers_in.get(common.HOST_HEADER) + if host is not None: + self._request.ws_location = _build_location(self._request) + # TODO(ukai): check host is this host. + + def _set_origin(self): + # |Origin| + origin = self._request.headers_in.get(common.ORIGIN_HEADER) + if origin is not None: + self._request.ws_origin = origin + + def _set_protocol_version(self): + # |Sec-WebSocket-Draft| + draft = self._request.headers_in.get(common.SEC_WEBSOCKET_DRAFT_HEADER) + if draft is not None and draft != '0': + raise HandshakeException('Illegal value for %s: %s' % + (common.SEC_WEBSOCKET_DRAFT_HEADER, + draft)) + + self._logger.debug('Protocol version is HyBi 00') + self._request.ws_version = common.VERSION_HYBI00 + self._request.ws_stream = StreamHixie75(self._request, True) + + def _set_challenge_response(self): + # 5.2 4-8. + self._request.ws_challenge = self._get_challenge() + # 5.2 9. let /response/ be the MD5 finterprint of /challenge/ + self._request.ws_challenge_md5 = util.md5_hash( + self._request.ws_challenge).digest() + self._logger.debug( + 'Challenge: %r (%s)', + self._request.ws_challenge, + util.hexify(self._request.ws_challenge)) + self._logger.debug( + 'Challenge response: %r (%s)', + self._request.ws_challenge_md5, + util.hexify(self._request.ws_challenge_md5)) + + def _get_key_value(self, key_field): + key_value = get_mandatory_header(self._request, key_field) + + self._logger.debug('%s: %r', key_field, key_value) + + # 5.2 4. let /key-number_n/ be the digits (characters in the range + # U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9)) in /key_n/, + # interpreted as a base ten integer, ignoring all other characters + # in /key_n/. + try: + key_number = int(re.sub("\\D", "", key_value)) + except: + raise HandshakeException('%s field contains no digit' % key_field) + # 5.2 5. let /spaces_n/ be the number of U+0020 SPACE characters + # in /key_n/. + spaces = re.subn(" ", "", key_value)[1] + if spaces == 0: + raise HandshakeException('%s field contains no space' % key_field) + + self._logger.debug( + '%s: Key-number is %d and number of spaces is %d', + key_field, key_number, spaces) + + # 5.2 6. if /key-number_n/ is not an integral multiple of /spaces_n/ + # then abort the WebSocket connection. + if key_number % spaces != 0: + raise HandshakeException( + '%s: Key-number (%d) is not an integral multiple of spaces ' + '(%d)' % (key_field, key_number, spaces)) + # 5.2 7. let /part_n/ be /key-number_n/ divided by /spaces_n/. + part = key_number / spaces + self._logger.debug('%s: Part is %d', key_field, part) + return part + + def _get_challenge(self): + # 5.2 4-7. + key1 = self._get_key_value(common.SEC_WEBSOCKET_KEY1_HEADER) + key2 = self._get_key_value(common.SEC_WEBSOCKET_KEY2_HEADER) + # 5.2 8. let /challenge/ be the concatenation of /part_1/, + challenge = '' + challenge += struct.pack('!I', key1) # network byteorder int + challenge += struct.pack('!I', key2) # network byteorder int + challenge += self._request.connection.read(8) + return challenge + + def _send_handshake(self): + response = [] + + # 5.2 10. send the following line. + response.append('HTTP/1.1 101 WebSocket Protocol Handshake\r\n') + + # 5.2 11. send the following fields to the client. + response.append(format_header( + common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75)) + response.append(format_header( + common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE)) + response.append(format_header( + common.SEC_WEBSOCKET_LOCATION_HEADER, self._request.ws_location)) + response.append(format_header( + common.SEC_WEBSOCKET_ORIGIN_HEADER, self._request.ws_origin)) + if self._request.ws_protocol: + response.append(format_header( + common.SEC_WEBSOCKET_PROTOCOL_HEADER, + self._request.ws_protocol)) + # 5.2 12. send two bytes 0x0D 0x0A. + response.append('\r\n') + # 5.2 13. send /response/ + response.append(self._request.ws_challenge_md5) + + raw_response = ''.join(response) + self._request.connection.write(raw_response) + self._logger.debug('Sent server\'s opening handshake: %r', + raw_response) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/headerparserhandler.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/headerparserhandler.py new file mode 100644 index 000000000..c244421cf --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/headerparserhandler.py @@ -0,0 +1,254 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""PythonHeaderParserHandler for mod_pywebsocket. + +Apache HTTP Server and mod_python must be configured such that this +function is called to handle WebSocket request. +""" + + +import logging + +from mod_python import apache + +from mod_pywebsocket import common +from mod_pywebsocket import dispatch +from mod_pywebsocket import handshake +from mod_pywebsocket import util + + +# PythonOption to specify the handler root directory. +_PYOPT_HANDLER_ROOT = 'mod_pywebsocket.handler_root' + +# PythonOption to specify the handler scan directory. +# This must be a directory under the root directory. +# The default is the root directory. +_PYOPT_HANDLER_SCAN = 'mod_pywebsocket.handler_scan' + +# PythonOption to allow handlers whose canonical path is +# not under the root directory. It's disallowed by default. +# Set this option with value of 'yes' to allow. +_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT = ( + 'mod_pywebsocket.allow_handlers_outside_root_dir') +# Map from values to their meanings. 'Yes' and 'No' are allowed just for +# compatibility. +_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT_DEFINITION = { + 'off': False, 'no': False, 'on': True, 'yes': True} + +# (Obsolete option. Ignored.) +# PythonOption to specify to allow handshake defined in Hixie 75 version +# protocol. The default is None (Off) +_PYOPT_ALLOW_DRAFT75 = 'mod_pywebsocket.allow_draft75' +# Map from values to their meanings. +_PYOPT_ALLOW_DRAFT75_DEFINITION = {'off': False, 'on': True} + + +class ApacheLogHandler(logging.Handler): + """Wrapper logging.Handler to emit log message to apache's error.log.""" + + _LEVELS = { + logging.DEBUG: apache.APLOG_DEBUG, + logging.INFO: apache.APLOG_INFO, + logging.WARNING: apache.APLOG_WARNING, + logging.ERROR: apache.APLOG_ERR, + logging.CRITICAL: apache.APLOG_CRIT, + } + + def __init__(self, request=None): + logging.Handler.__init__(self) + self._log_error = apache.log_error + if request is not None: + self._log_error = request.log_error + + # Time and level will be printed by Apache. + self._formatter = logging.Formatter('%(name)s: %(message)s') + + def emit(self, record): + apache_level = apache.APLOG_DEBUG + if record.levelno in ApacheLogHandler._LEVELS: + apache_level = ApacheLogHandler._LEVELS[record.levelno] + + msg = self._formatter.format(record) + + # "server" parameter must be passed to have "level" parameter work. + # If only "level" parameter is passed, nothing shows up on Apache's + # log. However, at this point, we cannot get the server object of the + # virtual host which will process WebSocket requests. The only server + # object we can get here is apache.main_server. But Wherever (server + # configuration context or virtual host context) we put + # PythonHeaderParserHandler directive, apache.main_server just points + # the main server instance (not any of virtual server instance). Then, + # Apache follows LogLevel directive in the server configuration context + # to filter logs. So, we need to specify LogLevel in the server + # configuration context. Even if we specify "LogLevel debug" in the + # virtual host context which actually handles WebSocket connections, + # DEBUG level logs never show up unless "LogLevel debug" is specified + # in the server configuration context. + # + # TODO(tyoshino): Provide logging methods on request object. When + # request is mp_request object (when used together with Apache), the + # methods call request.log_error indirectly. When request is + # _StandaloneRequest, the methods call Python's logging facility which + # we create in standalone.py. + self._log_error(msg, apache_level, apache.main_server) + + +def _configure_logging(): + logger = logging.getLogger() + # Logs are filtered by Apache based on LogLevel directive in Apache + # configuration file. We must just pass logs for all levels to + # ApacheLogHandler. + logger.setLevel(logging.DEBUG) + logger.addHandler(ApacheLogHandler()) + + +_configure_logging() + +_LOGGER = logging.getLogger(__name__) + + +def _parse_option(name, value, definition): + if value is None: + return False + + meaning = definition.get(value.lower()) + if meaning is None: + raise Exception('Invalid value for PythonOption %s: %r' % + (name, value)) + return meaning + + +def _create_dispatcher(): + _LOGGER.info('Initializing Dispatcher') + + options = apache.main_server.get_options() + + handler_root = options.get(_PYOPT_HANDLER_ROOT, None) + if not handler_root: + raise Exception('PythonOption %s is not defined' % _PYOPT_HANDLER_ROOT, + apache.APLOG_ERR) + + handler_scan = options.get(_PYOPT_HANDLER_SCAN, handler_root) + + allow_handlers_outside_root = _parse_option( + _PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT, + options.get(_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT), + _PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT_DEFINITION) + + dispatcher = dispatch.Dispatcher( + handler_root, handler_scan, allow_handlers_outside_root) + + for warning in dispatcher.source_warnings(): + apache.log_error( + 'mod_pywebsocket: Warning in source loading: %s' % warning, + apache.APLOG_WARNING) + + return dispatcher + + +# Initialize +_dispatcher = _create_dispatcher() + + +def headerparserhandler(request): + """Handle request. + + Args: + request: mod_python request. + + This function is named headerparserhandler because it is the default + name for a PythonHeaderParserHandler. + """ + + handshake_is_done = False + try: + # Fallback to default http handler for request paths for which + # we don't have request handlers. + if not _dispatcher.get_handler_suite(request.uri): + request.log_error( + 'mod_pywebsocket: No handler for resource: %r' % request.uri, + apache.APLOG_INFO) + request.log_error( + 'mod_pywebsocket: Fallback to Apache', apache.APLOG_INFO) + return apache.DECLINED + except dispatch.DispatchException, e: + request.log_error( + 'mod_pywebsocket: Dispatch failed for error: %s' % e, + apache.APLOG_INFO) + if not handshake_is_done: + return e.status + + try: + allow_draft75 = _parse_option( + _PYOPT_ALLOW_DRAFT75, + apache.main_server.get_options().get(_PYOPT_ALLOW_DRAFT75), + _PYOPT_ALLOW_DRAFT75_DEFINITION) + + try: + handshake.do_handshake( + request, _dispatcher, allowDraft75=allow_draft75) + except handshake.VersionException, e: + request.log_error( + 'mod_pywebsocket: Handshake failed for version error: %s' % e, + apache.APLOG_INFO) + request.err_headers_out.add(common.SEC_WEBSOCKET_VERSION_HEADER, + e.supported_versions) + return apache.HTTP_BAD_REQUEST + except handshake.HandshakeException, e: + # Handshake for ws/wss failed. + # Send http response with error status. + request.log_error( + 'mod_pywebsocket: Handshake failed for error: %s' % e, + apache.APLOG_INFO) + return e.status + + handshake_is_done = True + request._dispatcher = _dispatcher + _dispatcher.transfer_data(request) + except handshake.AbortedByUserException, e: + request.log_error('mod_pywebsocket: Aborted: %s' % e, apache.APLOG_INFO) + except Exception, e: + # DispatchException can also be thrown if something is wrong in + # pywebsocket code. It's caught here, then. + + request.log_error('mod_pywebsocket: Exception occurred: %s\n%s' % + (e, util.get_stack_trace()), + apache.APLOG_ERR) + # Unknown exceptions before handshake mean Apache must handle its + # request with another handler. + if not handshake_is_done: + return apache.DECLINED + # Set assbackwards to suppress response header generation by Apache. + request.assbackwards = 1 + return apache.DONE # Return DONE such that no other handlers are invoked. + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/http_header_util.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/http_header_util.py new file mode 100644 index 000000000..b77465393 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/http_header_util.py @@ -0,0 +1,263 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Utilities for parsing and formatting headers that follow the grammar defined +in HTTP RFC http://www.ietf.org/rfc/rfc2616.txt. +""" + + +import urlparse + + +_SEPARATORS = '()<>@,;:\\"/[]?={} \t' + + +def _is_char(c): + """Returns true iff c is in CHAR as specified in HTTP RFC.""" + + return ord(c) <= 127 + + +def _is_ctl(c): + """Returns true iff c is in CTL as specified in HTTP RFC.""" + + return ord(c) <= 31 or ord(c) == 127 + + +class ParsingState(object): + + def __init__(self, data): + self.data = data + self.head = 0 + + +def peek(state, pos=0): + """Peeks the character at pos from the head of data.""" + + if state.head + pos >= len(state.data): + return None + + return state.data[state.head + pos] + + +def consume(state, amount=1): + """Consumes specified amount of bytes from the head and returns the + consumed bytes. If there's not enough bytes to consume, returns None. + """ + + if state.head + amount > len(state.data): + return None + + result = state.data[state.head:state.head + amount] + state.head = state.head + amount + return result + + +def consume_string(state, expected): + """Given a parsing state and a expected string, consumes the string from + the head. Returns True if consumed successfully. Otherwise, returns + False. + """ + + pos = 0 + + for c in expected: + if c != peek(state, pos): + return False + pos += 1 + + consume(state, pos) + return True + + +def consume_lws(state): + """Consumes a LWS from the head. Returns True if any LWS is consumed. + Otherwise, returns False. + + LWS = [CRLF] 1*( SP | HT ) + """ + + original_head = state.head + + consume_string(state, '\r\n') + + pos = 0 + + while True: + c = peek(state, pos) + if c == ' ' or c == '\t': + pos += 1 + else: + if pos == 0: + state.head = original_head + return False + else: + consume(state, pos) + return True + + +def consume_lwses(state): + """Consumes *LWS from the head.""" + + while consume_lws(state): + pass + + +def consume_token(state): + """Consumes a token from the head. Returns the token or None if no token + was found. + """ + + pos = 0 + + while True: + c = peek(state, pos) + if c is None or c in _SEPARATORS or _is_ctl(c) or not _is_char(c): + if pos == 0: + return None + + return consume(state, pos) + else: + pos += 1 + + +def consume_token_or_quoted_string(state): + """Consumes a token or a quoted-string, and returns the token or unquoted + string. If no token or quoted-string was found, returns None. + """ + + original_head = state.head + + if not consume_string(state, '"'): + return consume_token(state) + + result = [] + + expect_quoted_pair = False + + while True: + if not expect_quoted_pair and consume_lws(state): + result.append(' ') + continue + + c = consume(state) + if c is None: + # quoted-string is not enclosed with double quotation + state.head = original_head + return None + elif expect_quoted_pair: + expect_quoted_pair = False + if _is_char(c): + result.append(c) + else: + # Non CHAR character found in quoted-pair + state.head = original_head + return None + elif c == '\\': + expect_quoted_pair = True + elif c == '"': + return ''.join(result) + elif _is_ctl(c): + # Invalid character %r found in qdtext + state.head = original_head + return None + else: + result.append(c) + + +def quote_if_necessary(s): + """Quotes arbitrary string into quoted-string.""" + + quote = False + if s == '': + return '""' + + result = [] + for c in s: + if c == '"' or c in _SEPARATORS or _is_ctl(c) or not _is_char(c): + quote = True + + if c == '"' or _is_ctl(c): + result.append('\\' + c) + else: + result.append(c) + + if quote: + return '"' + ''.join(result) + '"' + else: + return ''.join(result) + + +def parse_uri(uri): + """Parse absolute URI then return host, port and resource.""" + + parsed = urlparse.urlsplit(uri) + if parsed.scheme != 'wss' and parsed.scheme != 'ws': + # |uri| must be a relative URI. + # TODO(toyoshim): Should validate |uri|. + return None, None, uri + + if parsed.hostname is None: + return None, None, None + + port = None + try: + port = parsed.port + except ValueError, e: + # port property cause ValueError on invalid null port description like + # 'ws://host:/path'. + return None, None, None + + if port is None: + if parsed.scheme == 'ws': + port = 80 + else: + port = 443 + + path = parsed.path + if not path: + path += '/' + if parsed.query: + path += '?' + parsed.query + if parsed.fragment: + path += '#' + parsed.fragment + + return parsed.hostname, port, path + + +try: + urlparse.uses_netloc.index('ws') +except ValueError, e: + # urlparse in Python2.5.1 doesn't have 'ws' and 'wss' entries. + urlparse.uses_netloc.append('ws') + urlparse.uses_netloc.append('wss') + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/memorizingfile.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/memorizingfile.py new file mode 100644 index 000000000..4d4cd9585 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/memorizingfile.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Memorizing file. + +A memorizing file wraps a file and memorizes lines read by readline. +""" + + +import sys + + +class MemorizingFile(object): + """MemorizingFile wraps a file and memorizes lines read by readline. + + Note that data read by other methods are not memorized. This behavior + is good enough for memorizing lines SimpleHTTPServer reads before + the control reaches WebSocketRequestHandler. + """ + + def __init__(self, file_, max_memorized_lines=sys.maxint): + """Construct an instance. + + Args: + file_: the file object to wrap. + max_memorized_lines: the maximum number of lines to memorize. + Only the first max_memorized_lines are memorized. + Default: sys.maxint. + """ + + self._file = file_ + self._memorized_lines = [] + self._max_memorized_lines = max_memorized_lines + self._buffered = False + self._buffered_line = None + + def __getattribute__(self, name): + if name in ('_file', '_memorized_lines', '_max_memorized_lines', + '_buffered', '_buffered_line', 'readline', + 'get_memorized_lines'): + return object.__getattribute__(self, name) + return self._file.__getattribute__(name) + + def readline(self, size=-1): + """Override file.readline and memorize the line read. + + Note that even if size is specified and smaller than actual size, + the whole line will be read out from underlying file object by + subsequent readline calls. + """ + + if self._buffered: + line = self._buffered_line + self._buffered = False + else: + line = self._file.readline() + if line and len(self._memorized_lines) < self._max_memorized_lines: + self._memorized_lines.append(line) + if size >= 0 and size < len(line): + self._buffered = True + self._buffered_line = line[size:] + return line[:size] + return line + + def get_memorized_lines(self): + """Get lines memorized so far.""" + return self._memorized_lines + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/msgutil.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/msgutil.py new file mode 100644 index 000000000..4c1a0114b --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/msgutil.py @@ -0,0 +1,219 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Message related utilities. + +Note: request.connection.write/read are used in this module, even though +mod_python document says that they should be used only in connection +handlers. Unfortunately, we have no other options. For example, +request.write/read are not suitable because they don't allow direct raw +bytes writing/reading. +""" + + +import Queue +import threading + + +# Export Exception symbols from msgutil for backward compatibility +from mod_pywebsocket._stream_base import ConnectionTerminatedException +from mod_pywebsocket._stream_base import InvalidFrameException +from mod_pywebsocket._stream_base import BadOperationException +from mod_pywebsocket._stream_base import UnsupportedFrameException + + +# An API for handler to send/receive WebSocket messages. +def close_connection(request): + """Close connection. + + Args: + request: mod_python request. + """ + request.ws_stream.close_connection() + + +def send_message(request, payload_data, end=True, binary=False): + """Send a message (or part of a message). + + Args: + request: mod_python request. + payload_data: unicode text or str binary to send. + end: True to terminate a message. + False to send payload_data as part of a message that is to be + terminated by next or later send_message call with end=True. + binary: send payload_data as binary frame(s). + Raises: + BadOperationException: when server already terminated. + """ + request.ws_stream.send_message(payload_data, end, binary) + + +def receive_message(request): + """Receive a WebSocket frame and return its payload as a text in + unicode or a binary in str. + + Args: + request: mod_python request. + Raises: + InvalidFrameException: when client send invalid frame. + UnsupportedFrameException: when client send unsupported frame e.g. some + of reserved bit is set but no extension can + recognize it. + InvalidUTF8Exception: when client send a text frame containing any + invalid UTF-8 string. + ConnectionTerminatedException: when the connection is closed + unexpectedly. + BadOperationException: when client already terminated. + """ + return request.ws_stream.receive_message() + + +def send_ping(request, body=''): + request.ws_stream.send_ping(body) + + +class MessageReceiver(threading.Thread): + """This class receives messages from the client. + + This class provides three ways to receive messages: blocking, + non-blocking, and via callback. Callback has the highest precedence. + + Note: This class should not be used with the standalone server for wss + because pyOpenSSL used by the server raises a fatal error if the socket + is accessed from multiple threads. + """ + + def __init__(self, request, onmessage=None): + """Construct an instance. + + Args: + request: mod_python request. + onmessage: a function to be called when a message is received. + May be None. If not None, the function is called on + another thread. In that case, MessageReceiver.receive + and MessageReceiver.receive_nowait are useless + because they will never return any messages. + """ + + threading.Thread.__init__(self) + self._request = request + self._queue = Queue.Queue() + self._onmessage = onmessage + self._stop_requested = False + self.setDaemon(True) + self.start() + + def run(self): + try: + while not self._stop_requested: + message = receive_message(self._request) + if self._onmessage: + self._onmessage(message) + else: + self._queue.put(message) + finally: + close_connection(self._request) + + def receive(self): + """ Receive a message from the channel, blocking. + + Returns: + message as a unicode string. + """ + return self._queue.get() + + def receive_nowait(self): + """ Receive a message from the channel, non-blocking. + + Returns: + message as a unicode string if available. None otherwise. + """ + try: + message = self._queue.get_nowait() + except Queue.Empty: + message = None + return message + + def stop(self): + """Request to stop this instance. + + The instance will be stopped after receiving the next message. + This method may not be very useful, but there is no clean way + in Python to forcefully stop a running thread. + """ + self._stop_requested = True + + +class MessageSender(threading.Thread): + """This class sends messages to the client. + + This class provides both synchronous and asynchronous ways to send + messages. + + Note: This class should not be used with the standalone server for wss + because pyOpenSSL used by the server raises a fatal error if the socket + is accessed from multiple threads. + """ + + def __init__(self, request): + """Construct an instance. + + Args: + request: mod_python request. + """ + threading.Thread.__init__(self) + self._request = request + self._queue = Queue.Queue() + self.setDaemon(True) + self.start() + + def run(self): + while True: + message, condition = self._queue.get() + condition.acquire() + send_message(self._request, message) + condition.notify() + condition.release() + + def send(self, message): + """Send a message, blocking.""" + + condition = threading.Condition() + condition.acquire() + self._queue.put((message, condition)) + condition.wait() + + def send_nowait(self, message): + """Send a message, non-blocking.""" + + self._queue.put((message, threading.Condition())) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/mux.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/mux.py new file mode 100644 index 000000000..76334685b --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/mux.py @@ -0,0 +1,1889 @@ +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""This file provides classes and helper functions for multiplexing extension. + +Specification: +http://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-06 +""" + + +import collections +import copy +import email +import email.parser +import logging +import math +import struct +import threading +import traceback + +from mod_pywebsocket import common +from mod_pywebsocket import handshake +from mod_pywebsocket import util +from mod_pywebsocket._stream_base import BadOperationException +from mod_pywebsocket._stream_base import ConnectionTerminatedException +from mod_pywebsocket._stream_base import InvalidFrameException +from mod_pywebsocket._stream_hybi import Frame +from mod_pywebsocket._stream_hybi import Stream +from mod_pywebsocket._stream_hybi import StreamOptions +from mod_pywebsocket._stream_hybi import create_binary_frame +from mod_pywebsocket._stream_hybi import create_closing_handshake_body +from mod_pywebsocket._stream_hybi import create_header +from mod_pywebsocket._stream_hybi import create_length_header +from mod_pywebsocket._stream_hybi import parse_frame +from mod_pywebsocket.handshake import hybi + + +_CONTROL_CHANNEL_ID = 0 +_DEFAULT_CHANNEL_ID = 1 + +_MUX_OPCODE_ADD_CHANNEL_REQUEST = 0 +_MUX_OPCODE_ADD_CHANNEL_RESPONSE = 1 +_MUX_OPCODE_FLOW_CONTROL = 2 +_MUX_OPCODE_DROP_CHANNEL = 3 +_MUX_OPCODE_NEW_CHANNEL_SLOT = 4 + +_MAX_CHANNEL_ID = 2 ** 29 - 1 + +_INITIAL_NUMBER_OF_CHANNEL_SLOTS = 64 +_INITIAL_QUOTA_FOR_CLIENT = 8 * 1024 + +_HANDSHAKE_ENCODING_IDENTITY = 0 +_HANDSHAKE_ENCODING_DELTA = 1 + +# We need only these status code for now. +_HTTP_BAD_RESPONSE_MESSAGES = { + common.HTTP_STATUS_BAD_REQUEST: 'Bad Request', +} + +# DropChannel reason code +# TODO(bashi): Define all reason code defined in -05 draft. +_DROP_CODE_NORMAL_CLOSURE = 1000 + +_DROP_CODE_INVALID_ENCAPSULATING_MESSAGE = 2001 +_DROP_CODE_CHANNEL_ID_TRUNCATED = 2002 +_DROP_CODE_ENCAPSULATED_FRAME_IS_TRUNCATED = 2003 +_DROP_CODE_UNKNOWN_MUX_OPCODE = 2004 +_DROP_CODE_INVALID_MUX_CONTROL_BLOCK = 2005 +_DROP_CODE_CHANNEL_ALREADY_EXISTS = 2006 +_DROP_CODE_NEW_CHANNEL_SLOT_VIOLATION = 2007 +_DROP_CODE_UNKNOWN_REQUEST_ENCODING = 2010 + +_DROP_CODE_SEND_QUOTA_VIOLATION = 3005 +_DROP_CODE_SEND_QUOTA_OVERFLOW = 3006 +_DROP_CODE_ACKNOWLEDGED = 3008 +_DROP_CODE_BAD_FRAGMENTATION = 3009 + + +class MuxUnexpectedException(Exception): + """Exception in handling multiplexing extension.""" + pass + + +# Temporary +class MuxNotImplementedException(Exception): + """Raised when a flow enters unimplemented code path.""" + pass + + +class LogicalConnectionClosedException(Exception): + """Raised when logical connection is gracefully closed.""" + pass + + +class PhysicalConnectionError(Exception): + """Raised when there is a physical connection error.""" + def __init__(self, drop_code, message=''): + super(PhysicalConnectionError, self).__init__( + 'code=%d, message=%r' % (drop_code, message)) + self.drop_code = drop_code + self.message = message + + +class LogicalChannelError(Exception): + """Raised when there is a logical channel error.""" + def __init__(self, channel_id, drop_code, message=''): + super(LogicalChannelError, self).__init__( + 'channel_id=%d, code=%d, message=%r' % ( + channel_id, drop_code, message)) + self.channel_id = channel_id + self.drop_code = drop_code + self.message = message + + +def _encode_channel_id(channel_id): + if channel_id < 0: + raise ValueError('Channel id %d must not be negative' % channel_id) + + if channel_id < 2 ** 7: + return chr(channel_id) + if channel_id < 2 ** 14: + return struct.pack('!H', 0x8000 + channel_id) + if channel_id < 2 ** 21: + first = chr(0xc0 + (channel_id >> 16)) + return first + struct.pack('!H', channel_id & 0xffff) + if channel_id < 2 ** 29: + return struct.pack('!L', 0xe0000000 + channel_id) + + raise ValueError('Channel id %d is too large' % channel_id) + + +def _encode_number(number): + return create_length_header(number, False) + + +def _create_add_channel_response(channel_id, encoded_handshake, + encoding=0, rejected=False): + if encoding != 0 and encoding != 1: + raise ValueError('Invalid encoding %d' % encoding) + + first_byte = ((_MUX_OPCODE_ADD_CHANNEL_RESPONSE << 5) | + (rejected << 4) | encoding) + block = (chr(first_byte) + + _encode_channel_id(channel_id) + + _encode_number(len(encoded_handshake)) + + encoded_handshake) + return block + + +def _create_drop_channel(channel_id, code=None, message=''): + if len(message) > 0 and code is None: + raise ValueError('Code must be specified if message is specified') + + first_byte = _MUX_OPCODE_DROP_CHANNEL << 5 + block = chr(first_byte) + _encode_channel_id(channel_id) + if code is None: + block += _encode_number(0) # Reason size + else: + reason = struct.pack('!H', code) + message + reason_size = _encode_number(len(reason)) + block += reason_size + reason + + return block + + +def _create_flow_control(channel_id, replenished_quota): + first_byte = _MUX_OPCODE_FLOW_CONTROL << 5 + block = (chr(first_byte) + + _encode_channel_id(channel_id) + + _encode_number(replenished_quota)) + return block + + +def _create_new_channel_slot(slots, send_quota): + if slots < 0 or send_quota < 0: + raise ValueError('slots and send_quota must be non-negative.') + first_byte = _MUX_OPCODE_NEW_CHANNEL_SLOT << 5 + block = (chr(first_byte) + + _encode_number(slots) + + _encode_number(send_quota)) + return block + + +def _create_fallback_new_channel_slot(): + first_byte = (_MUX_OPCODE_NEW_CHANNEL_SLOT << 5) | 1 # Set the F flag + block = (chr(first_byte) + _encode_number(0) + _encode_number(0)) + return block + + +def _parse_request_text(request_text): + request_line, header_lines = request_text.split('\r\n', 1) + + words = request_line.split(' ') + if len(words) != 3: + raise ValueError('Bad Request-Line syntax %r' % request_line) + [command, path, version] = words + if version != 'HTTP/1.1': + raise ValueError('Bad request version %r' % version) + + # email.parser.Parser() parses RFC 2822 (RFC 822) style headers. + # RFC 6455 refers RFC 2616 for handshake parsing, and RFC 2616 refers + # RFC 822. + headers = email.parser.Parser().parsestr(header_lines) + return command, path, version, headers + + +class _ControlBlock(object): + """A structure that holds parsing result of multiplexing control block. + Control block specific attributes will be added by _MuxFramePayloadParser. + (e.g. encoded_handshake will be added for AddChannelRequest and + AddChannelResponse) + """ + + def __init__(self, opcode): + self.opcode = opcode + + +class _MuxFramePayloadParser(object): + """A class that parses multiplexed frame payload.""" + + def __init__(self, payload): + self._data = payload + self._read_position = 0 + self._logger = util.get_class_logger(self) + + def read_channel_id(self): + """Reads channel id. + + Raises: + ValueError: when the payload doesn't contain + valid channel id. + """ + + remaining_length = len(self._data) - self._read_position + pos = self._read_position + if remaining_length == 0: + raise ValueError('Invalid channel id format') + + channel_id = ord(self._data[pos]) + channel_id_length = 1 + if channel_id & 0xe0 == 0xe0: + if remaining_length < 4: + raise ValueError('Invalid channel id format') + channel_id = struct.unpack('!L', + self._data[pos:pos+4])[0] & 0x1fffffff + channel_id_length = 4 + elif channel_id & 0xc0 == 0xc0: + if remaining_length < 3: + raise ValueError('Invalid channel id format') + channel_id = (((channel_id & 0x1f) << 16) + + struct.unpack('!H', self._data[pos+1:pos+3])[0]) + channel_id_length = 3 + elif channel_id & 0x80 == 0x80: + if remaining_length < 2: + raise ValueError('Invalid channel id format') + channel_id = struct.unpack('!H', + self._data[pos:pos+2])[0] & 0x3fff + channel_id_length = 2 + self._read_position += channel_id_length + + return channel_id + + def read_inner_frame(self): + """Reads an inner frame. + + Raises: + PhysicalConnectionError: when the inner frame is invalid. + """ + + if len(self._data) == self._read_position: + raise PhysicalConnectionError( + _DROP_CODE_ENCAPSULATED_FRAME_IS_TRUNCATED) + + bits = ord(self._data[self._read_position]) + self._read_position += 1 + fin = (bits & 0x80) == 0x80 + rsv1 = (bits & 0x40) == 0x40 + rsv2 = (bits & 0x20) == 0x20 + rsv3 = (bits & 0x10) == 0x10 + opcode = bits & 0xf + payload = self.remaining_data() + # Consume rest of the message which is payload data of the original + # frame. + self._read_position = len(self._data) + return fin, rsv1, rsv2, rsv3, opcode, payload + + def _read_number(self): + if self._read_position + 1 > len(self._data): + raise ValueError( + 'Cannot read the first byte of number field') + + number = ord(self._data[self._read_position]) + if number & 0x80 == 0x80: + raise ValueError( + 'The most significant bit of the first byte of number should ' + 'be unset') + self._read_position += 1 + pos = self._read_position + if number == 127: + if pos + 8 > len(self._data): + raise ValueError('Invalid number field') + self._read_position += 8 + number = struct.unpack('!Q', self._data[pos:pos+8])[0] + if number > 0x7FFFFFFFFFFFFFFF: + raise ValueError('Encoded number(%d) >= 2^63' % number) + if number <= 0xFFFF: + raise ValueError( + '%d should not be encoded by 9 bytes encoding' % number) + return number + if number == 126: + if pos + 2 > len(self._data): + raise ValueError('Invalid number field') + self._read_position += 2 + number = struct.unpack('!H', self._data[pos:pos+2])[0] + if number <= 125: + raise ValueError( + '%d should not be encoded by 3 bytes encoding' % number) + return number + + def _read_size_and_contents(self): + """Reads data that consists of followings: + - the size of the contents encoded the same way as payload length + of the WebSocket Protocol with 1 bit padding at the head. + - the contents. + """ + + try: + size = self._read_number() + except ValueError, e: + raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + str(e)) + pos = self._read_position + if pos + size > len(self._data): + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Cannot read %d bytes data' % size) + + self._read_position += size + return self._data[pos:pos+size] + + def _read_add_channel_request(self, first_byte, control_block): + reserved = (first_byte >> 2) & 0x7 + if reserved != 0: + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Reserved bits must be unset') + + # Invalid encoding will be handled by MuxHandler. + encoding = first_byte & 0x3 + try: + control_block.channel_id = self.read_channel_id() + except ValueError, e: + raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK) + control_block.encoding = encoding + encoded_handshake = self._read_size_and_contents() + control_block.encoded_handshake = encoded_handshake + return control_block + + def _read_add_channel_response(self, first_byte, control_block): + reserved = (first_byte >> 2) & 0x3 + if reserved != 0: + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Reserved bits must be unset') + + control_block.accepted = (first_byte >> 4) & 1 + control_block.encoding = first_byte & 0x3 + try: + control_block.channel_id = self.read_channel_id() + except ValueError, e: + raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK) + control_block.encoded_handshake = self._read_size_and_contents() + return control_block + + def _read_flow_control(self, first_byte, control_block): + reserved = first_byte & 0x1f + if reserved != 0: + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Reserved bits must be unset') + + try: + control_block.channel_id = self.read_channel_id() + control_block.send_quota = self._read_number() + except ValueError, e: + raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + str(e)) + + return control_block + + def _read_drop_channel(self, first_byte, control_block): + reserved = first_byte & 0x1f + if reserved != 0: + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Reserved bits must be unset') + + try: + control_block.channel_id = self.read_channel_id() + except ValueError, e: + raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK) + reason = self._read_size_and_contents() + if len(reason) == 0: + control_block.drop_code = None + control_block.drop_message = '' + elif len(reason) >= 2: + control_block.drop_code = struct.unpack('!H', reason[:2])[0] + control_block.drop_message = reason[2:] + else: + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Received DropChannel that conains only 1-byte reason') + return control_block + + def _read_new_channel_slot(self, first_byte, control_block): + reserved = first_byte & 0x1e + if reserved != 0: + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Reserved bits must be unset') + control_block.fallback = first_byte & 1 + try: + control_block.slots = self._read_number() + control_block.send_quota = self._read_number() + except ValueError, e: + raise PhysicalConnectionError(_DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + str(e)) + return control_block + + def read_control_blocks(self): + """Reads control block(s). + + Raises: + PhysicalConnectionError: when the payload contains invalid control + block(s). + StopIteration: when no control blocks left. + """ + + while self._read_position < len(self._data): + first_byte = ord(self._data[self._read_position]) + self._read_position += 1 + opcode = (first_byte >> 5) & 0x7 + control_block = _ControlBlock(opcode=opcode) + if opcode == _MUX_OPCODE_ADD_CHANNEL_REQUEST: + yield self._read_add_channel_request(first_byte, control_block) + elif opcode == _MUX_OPCODE_ADD_CHANNEL_RESPONSE: + yield self._read_add_channel_response( + first_byte, control_block) + elif opcode == _MUX_OPCODE_FLOW_CONTROL: + yield self._read_flow_control(first_byte, control_block) + elif opcode == _MUX_OPCODE_DROP_CHANNEL: + yield self._read_drop_channel(first_byte, control_block) + elif opcode == _MUX_OPCODE_NEW_CHANNEL_SLOT: + yield self._read_new_channel_slot(first_byte, control_block) + else: + raise PhysicalConnectionError( + _DROP_CODE_UNKNOWN_MUX_OPCODE, + 'Invalid opcode %d' % opcode) + + assert self._read_position == len(self._data) + raise StopIteration + + def remaining_data(self): + """Returns remaining data.""" + + return self._data[self._read_position:] + + +class _LogicalRequest(object): + """Mimics mod_python request.""" + + def __init__(self, channel_id, command, path, protocol, headers, + connection): + """Constructs an instance. + + Args: + channel_id: the channel id of the logical channel. + command: HTTP request command. + path: HTTP request path. + headers: HTTP headers. + connection: _LogicalConnection instance. + """ + + self.channel_id = channel_id + self.method = command + self.uri = path + self.protocol = protocol + self.headers_in = headers + self.connection = connection + self.server_terminated = False + self.client_terminated = False + + def is_https(self): + """Mimics request.is_https(). Returns False because this method is + used only by old protocols (hixie and hybi00). + """ + + return False + + +class _LogicalConnection(object): + """Mimics mod_python mp_conn.""" + + # For details, see the comment of set_read_state(). + STATE_ACTIVE = 1 + STATE_GRACEFULLY_CLOSED = 2 + STATE_TERMINATED = 3 + + def __init__(self, mux_handler, channel_id): + """Constructs an instance. + + Args: + mux_handler: _MuxHandler instance. + channel_id: channel id of this connection. + """ + + self._mux_handler = mux_handler + self._channel_id = channel_id + self._incoming_data = '' + + # - Protects _waiting_write_completion + # - Signals the thread waiting for completion of write by mux handler + self._write_condition = threading.Condition() + self._waiting_write_completion = False + + self._read_condition = threading.Condition() + self._read_state = self.STATE_ACTIVE + + def get_local_addr(self): + """Getter to mimic mp_conn.local_addr.""" + + return self._mux_handler.physical_connection.get_local_addr() + local_addr = property(get_local_addr) + + def get_remote_addr(self): + """Getter to mimic mp_conn.remote_addr.""" + + return self._mux_handler.physical_connection.get_remote_addr() + remote_addr = property(get_remote_addr) + + def get_memorized_lines(self): + """Gets memorized lines. Not supported.""" + + raise MuxUnexpectedException('_LogicalConnection does not support ' + 'get_memorized_lines') + + def write(self, data): + """Writes data. mux_handler sends data asynchronously. The caller will + be suspended until write done. + + Args: + data: data to be written. + + Raises: + MuxUnexpectedException: when called before finishing the previous + write. + """ + + try: + self._write_condition.acquire() + if self._waiting_write_completion: + raise MuxUnexpectedException( + 'Logical connection %d is already waiting the completion ' + 'of write' % self._channel_id) + + self._waiting_write_completion = True + self._mux_handler.send_data(self._channel_id, data) + self._write_condition.wait() + # TODO(tyoshino): Raise an exception if woke up by on_writer_done. + finally: + self._write_condition.release() + + def write_control_data(self, data): + """Writes data via the control channel. Don't wait finishing write + because this method can be called by mux dispatcher. + + Args: + data: data to be written. + """ + + self._mux_handler.send_control_data(data) + + def on_write_data_done(self): + """Called when sending data is completed.""" + + try: + self._write_condition.acquire() + if not self._waiting_write_completion: + raise MuxUnexpectedException( + 'Invalid call of on_write_data_done for logical ' + 'connection %d' % self._channel_id) + self._waiting_write_completion = False + self._write_condition.notify() + finally: + self._write_condition.release() + + def on_writer_done(self): + """Called by the mux handler when the writer thread has finished.""" + + try: + self._write_condition.acquire() + self._waiting_write_completion = False + self._write_condition.notify() + finally: + self._write_condition.release() + + + def append_frame_data(self, frame_data): + """Appends incoming frame data. Called when mux_handler dispatches + frame data to the corresponding application. + + Args: + frame_data: incoming frame data. + """ + + self._read_condition.acquire() + self._incoming_data += frame_data + self._read_condition.notify() + self._read_condition.release() + + def read(self, length): + """Reads data. Blocks until enough data has arrived via physical + connection. + + Args: + length: length of data to be read. + Raises: + LogicalConnectionClosedException: when closing handshake for this + logical channel has been received. + ConnectionTerminatedException: when the physical connection has + closed, or an error is caused on the reader thread. + """ + + self._read_condition.acquire() + while (self._read_state == self.STATE_ACTIVE and + len(self._incoming_data) < length): + self._read_condition.wait() + + try: + if self._read_state == self.STATE_GRACEFULLY_CLOSED: + raise LogicalConnectionClosedException( + 'Logical channel %d has closed.' % self._channel_id) + elif self._read_state == self.STATE_TERMINATED: + raise ConnectionTerminatedException( + 'Receiving %d byte failed. Logical channel (%d) closed' % + (length, self._channel_id)) + + value = self._incoming_data[:length] + self._incoming_data = self._incoming_data[length:] + finally: + self._read_condition.release() + + return value + + def set_read_state(self, new_state): + """Sets the state of this connection. Called when an event for this + connection has occurred. + + Args: + new_state: state to be set. new_state must be one of followings: + - STATE_GRACEFULLY_CLOSED: when closing handshake for this + connection has been received. + - STATE_TERMINATED: when the physical connection has closed or + DropChannel of this connection has received. + """ + + self._read_condition.acquire() + self._read_state = new_state + self._read_condition.notify() + self._read_condition.release() + + +class _InnerMessage(object): + """Holds the result of _InnerMessageBuilder.build(). + """ + + def __init__(self, opcode, payload): + self.opcode = opcode + self.payload = payload + + +class _InnerMessageBuilder(object): + """A class that holds the context of inner message fragmentation and + builds a message from fragmented inner frame(s). + """ + + def __init__(self): + self._control_opcode = None + self._pending_control_fragments = [] + self._message_opcode = None + self._pending_message_fragments = [] + self._frame_handler = self._handle_first + + def _handle_first(self, frame): + if frame.opcode == common.OPCODE_CONTINUATION: + raise InvalidFrameException('Sending invalid continuation opcode') + + if common.is_control_opcode(frame.opcode): + return self._process_first_fragmented_control(frame) + else: + return self._process_first_fragmented_message(frame) + + def _process_first_fragmented_control(self, frame): + self._control_opcode = frame.opcode + self._pending_control_fragments.append(frame.payload) + if not frame.fin: + self._frame_handler = self._handle_fragmented_control + return None + return self._reassemble_fragmented_control() + + def _process_first_fragmented_message(self, frame): + self._message_opcode = frame.opcode + self._pending_message_fragments.append(frame.payload) + if not frame.fin: + self._frame_handler = self._handle_fragmented_message + return None + return self._reassemble_fragmented_message() + + def _handle_fragmented_control(self, frame): + if frame.opcode != common.OPCODE_CONTINUATION: + raise InvalidFrameException( + 'Sending invalid opcode %d while sending fragmented control ' + 'message' % frame.opcode) + self._pending_control_fragments.append(frame.payload) + if not frame.fin: + return None + return self._reassemble_fragmented_control() + + def _reassemble_fragmented_control(self): + opcode = self._control_opcode + payload = ''.join(self._pending_control_fragments) + self._control_opcode = None + self._pending_control_fragments = [] + if self._message_opcode is not None: + self._frame_handler = self._handle_fragmented_message + else: + self._frame_handler = self._handle_first + return _InnerMessage(opcode, payload) + + def _handle_fragmented_message(self, frame): + # Sender can interleave a control message while sending fragmented + # messages. + if common.is_control_opcode(frame.opcode): + if self._control_opcode is not None: + raise MuxUnexpectedException( + 'Should not reach here(Bug in builder)') + return self._process_first_fragmented_control(frame) + + if frame.opcode != common.OPCODE_CONTINUATION: + raise InvalidFrameException( + 'Sending invalid opcode %d while sending fragmented message' % + frame.opcode) + self._pending_message_fragments.append(frame.payload) + if not frame.fin: + return None + return self._reassemble_fragmented_message() + + def _reassemble_fragmented_message(self): + opcode = self._message_opcode + payload = ''.join(self._pending_message_fragments) + self._message_opcode = None + self._pending_message_fragments = [] + self._frame_handler = self._handle_first + return _InnerMessage(opcode, payload) + + def build(self, frame): + """Build an inner message. Returns an _InnerMessage instance when + the given frame is the last fragmented frame. Returns None otherwise. + + Args: + frame: an inner frame. + Raises: + InvalidFrameException: when received invalid opcode. (e.g. + receiving non continuation data opcode but the fin flag of + the previous inner frame was not set.) + """ + + return self._frame_handler(frame) + + +class _LogicalStream(Stream): + """Mimics the Stream class. This class interprets multiplexed WebSocket + frames. + """ + + def __init__(self, request, stream_options, send_quota, receive_quota): + """Constructs an instance. + + Args: + request: _LogicalRequest instance. + stream_options: StreamOptions instance. + send_quota: Initial send quota. + receive_quota: Initial receive quota. + """ + + # Physical stream is responsible for masking. + stream_options.unmask_receive = False + Stream.__init__(self, request, stream_options) + + self._send_closed = False + self._send_quota = send_quota + # - Protects _send_closed and _send_quota + # - Signals the thread waiting for send quota replenished + self._send_condition = threading.Condition() + + # The opcode of the first frame in messages. + self._message_opcode = common.OPCODE_TEXT + # True when the last message was fragmented. + self._last_message_was_fragmented = False + + self._receive_quota = receive_quota + self._write_inner_frame_semaphore = threading.Semaphore() + + self._inner_message_builder = _InnerMessageBuilder() + + def _create_inner_frame(self, opcode, payload, end=True): + frame = Frame(fin=end, opcode=opcode, payload=payload) + for frame_filter in self._options.outgoing_frame_filters: + frame_filter.filter(frame) + + if len(payload) != len(frame.payload): + raise MuxUnexpectedException( + 'Mux extension must not be used after extensions which change ' + ' frame boundary') + + first_byte = ((frame.fin << 7) | (frame.rsv1 << 6) | + (frame.rsv2 << 5) | (frame.rsv3 << 4) | frame.opcode) + return chr(first_byte) + frame.payload + + def _write_inner_frame(self, opcode, payload, end=True): + payload_length = len(payload) + write_position = 0 + + try: + # An inner frame will be fragmented if there is no enough send + # quota. This semaphore ensures that fragmented inner frames are + # sent in order on the logical channel. + # Note that frames that come from other logical channels or + # multiplexing control blocks can be inserted between fragmented + # inner frames on the physical channel. + self._write_inner_frame_semaphore.acquire() + + # Consume an octet quota when this is the first fragmented frame. + if opcode != common.OPCODE_CONTINUATION: + try: + self._send_condition.acquire() + while (not self._send_closed) and self._send_quota == 0: + self._send_condition.wait() + + if self._send_closed: + raise BadOperationException( + 'Logical connection %d is closed' % + self._request.channel_id) + + self._send_quota -= 1 + finally: + self._send_condition.release() + + while write_position < payload_length: + try: + self._send_condition.acquire() + while (not self._send_closed) and self._send_quota == 0: + self._logger.debug( + 'No quota. Waiting FlowControl message for %d.' % + self._request.channel_id) + self._send_condition.wait() + + if self._send_closed: + raise BadOperationException( + 'Logical connection %d is closed' % + self.request._channel_id) + + remaining = payload_length - write_position + write_length = min(self._send_quota, remaining) + inner_frame_end = ( + end and + (write_position + write_length == payload_length)) + + inner_frame = self._create_inner_frame( + opcode, + payload[write_position:write_position+write_length], + inner_frame_end) + self._send_quota -= write_length + self._logger.debug('Consumed quota=%d, remaining=%d' % + (write_length, self._send_quota)) + finally: + self._send_condition.release() + + # Writing data will block the worker so we need to release + # _send_condition before writing. + self._logger.debug('Sending inner frame: %r' % inner_frame) + self._request.connection.write(inner_frame) + write_position += write_length + + opcode = common.OPCODE_CONTINUATION + + except ValueError, e: + raise BadOperationException(e) + finally: + self._write_inner_frame_semaphore.release() + + def replenish_send_quota(self, send_quota): + """Replenish send quota.""" + + try: + self._send_condition.acquire() + if self._send_quota + send_quota > 0x7FFFFFFFFFFFFFFF: + self._send_quota = 0 + raise LogicalChannelError( + self._request.channel_id, _DROP_CODE_SEND_QUOTA_OVERFLOW) + self._send_quota += send_quota + self._logger.debug('Replenished send quota for channel id %d: %d' % + (self._request.channel_id, self._send_quota)) + finally: + self._send_condition.notify() + self._send_condition.release() + + def consume_receive_quota(self, amount): + """Consumes receive quota. Returns False on failure.""" + + if self._receive_quota < amount: + self._logger.debug('Violate quota on channel id %d: %d < %d' % + (self._request.channel_id, + self._receive_quota, amount)) + return False + self._receive_quota -= amount + return True + + def send_message(self, message, end=True, binary=False): + """Override Stream.send_message.""" + + if self._request.server_terminated: + raise BadOperationException( + 'Requested send_message after sending out a closing handshake') + + if binary and isinstance(message, unicode): + raise BadOperationException( + 'Message for binary frame must be instance of str') + + if binary: + opcode = common.OPCODE_BINARY + else: + opcode = common.OPCODE_TEXT + message = message.encode('utf-8') + + for message_filter in self._options.outgoing_message_filters: + message = message_filter.filter(message, end, binary) + + if self._last_message_was_fragmented: + if opcode != self._message_opcode: + raise BadOperationException('Message types are different in ' + 'frames for the same message') + opcode = common.OPCODE_CONTINUATION + else: + self._message_opcode = opcode + + self._write_inner_frame(opcode, message, end) + self._last_message_was_fragmented = not end + + def _receive_frame(self): + """Overrides Stream._receive_frame. + + In addition to call Stream._receive_frame, this method adds the amount + of payload to receiving quota and sends FlowControl to the client. + We need to do it here because Stream.receive_message() handles + control frames internally. + """ + + opcode, payload, fin, rsv1, rsv2, rsv3 = Stream._receive_frame(self) + amount = len(payload) + # Replenish extra one octet when receiving the first fragmented frame. + if opcode != common.OPCODE_CONTINUATION: + amount += 1 + self._receive_quota += amount + frame_data = _create_flow_control(self._request.channel_id, + amount) + self._logger.debug('Sending flow control for %d, replenished=%d' % + (self._request.channel_id, amount)) + self._request.connection.write_control_data(frame_data) + return opcode, payload, fin, rsv1, rsv2, rsv3 + + def _get_message_from_frame(self, frame): + """Overrides Stream._get_message_from_frame. + """ + + try: + inner_message = self._inner_message_builder.build(frame) + except InvalidFrameException: + raise LogicalChannelError( + self._request.channel_id, _DROP_CODE_BAD_FRAGMENTATION) + + if inner_message is None: + return None + self._original_opcode = inner_message.opcode + return inner_message.payload + + def receive_message(self): + """Overrides Stream.receive_message.""" + + # Just call Stream.receive_message(), but catch + # LogicalConnectionClosedException, which is raised when the logical + # connection has closed gracefully. + try: + return Stream.receive_message(self) + except LogicalConnectionClosedException, e: + self._logger.debug('%s', e) + return None + + def _send_closing_handshake(self, code, reason): + """Overrides Stream._send_closing_handshake.""" + + body = create_closing_handshake_body(code, reason) + self._logger.debug('Sending closing handshake for %d: (%r, %r)' % + (self._request.channel_id, code, reason)) + self._write_inner_frame(common.OPCODE_CLOSE, body, end=True) + + self._request.server_terminated = True + + def send_ping(self, body=''): + """Overrides Stream.send_ping""" + + self._logger.debug('Sending ping on logical channel %d: %r' % + (self._request.channel_id, body)) + self._write_inner_frame(common.OPCODE_PING, body, end=True) + + self._ping_queue.append(body) + + def _send_pong(self, body): + """Overrides Stream._send_pong""" + + self._logger.debug('Sending pong on logical channel %d: %r' % + (self._request.channel_id, body)) + self._write_inner_frame(common.OPCODE_PONG, body, end=True) + + def close_connection(self, code=common.STATUS_NORMAL_CLOSURE, reason=''): + """Overrides Stream.close_connection.""" + + # TODO(bashi): Implement + self._logger.debug('Closing logical connection %d' % + self._request.channel_id) + self._request.server_terminated = True + + def stop_sending(self): + """Stops accepting new send operation (_write_inner_frame).""" + + self._send_condition.acquire() + self._send_closed = True + self._send_condition.notify() + self._send_condition.release() + + +class _OutgoingData(object): + """A structure that holds data to be sent via physical connection and + origin of the data. + """ + + def __init__(self, channel_id, data): + self.channel_id = channel_id + self.data = data + + +class _PhysicalConnectionWriter(threading.Thread): + """A thread that is responsible for writing data to physical connection. + + TODO(bashi): Make sure there is no thread-safety problem when the reader + thread reads data from the same socket at a time. + """ + + def __init__(self, mux_handler): + """Constructs an instance. + + Args: + mux_handler: _MuxHandler instance. + """ + + threading.Thread.__init__(self) + self._logger = util.get_class_logger(self) + self._mux_handler = mux_handler + self.setDaemon(True) + + # When set, make this thread stop accepting new data, flush pending + # data and exit. + self._stop_requested = False + # The close code of the physical connection. + self._close_code = common.STATUS_NORMAL_CLOSURE + # Deque for passing write data. It's protected by _deque_condition + # until _stop_requested is set. + self._deque = collections.deque() + # - Protects _deque, _stop_requested and _close_code + # - Signals threads waiting for them to be available + self._deque_condition = threading.Condition() + + def put_outgoing_data(self, data): + """Puts outgoing data. + + Args: + data: _OutgoingData instance. + + Raises: + BadOperationException: when the thread has been requested to + terminate. + """ + + try: + self._deque_condition.acquire() + if self._stop_requested: + raise BadOperationException('Cannot write data anymore') + + self._deque.append(data) + self._deque_condition.notify() + finally: + self._deque_condition.release() + + def _write_data(self, outgoing_data): + message = (_encode_channel_id(outgoing_data.channel_id) + + outgoing_data.data) + try: + self._mux_handler.physical_stream.send_message( + message=message, end=True, binary=True) + except Exception, e: + util.prepend_message_to_exception( + 'Failed to send message to %r: ' % + (self._mux_handler.physical_connection.remote_addr,), e) + raise + + # TODO(bashi): It would be better to block the thread that sends + # control data as well. + if outgoing_data.channel_id != _CONTROL_CHANNEL_ID: + self._mux_handler.notify_write_data_done(outgoing_data.channel_id) + + def run(self): + try: + self._deque_condition.acquire() + while not self._stop_requested: + if len(self._deque) == 0: + self._deque_condition.wait() + continue + + outgoing_data = self._deque.popleft() + + self._deque_condition.release() + self._write_data(outgoing_data) + self._deque_condition.acquire() + + # Flush deque. + # + # At this point, self._deque_condition is always acquired. + try: + while len(self._deque) > 0: + outgoing_data = self._deque.popleft() + self._write_data(outgoing_data) + finally: + self._deque_condition.release() + + # Close physical connection. + try: + # Don't wait the response here. The response will be read + # by the reader thread. + self._mux_handler.physical_stream.close_connection( + self._close_code, wait_response=False) + except Exception, e: + util.prepend_message_to_exception( + 'Failed to close the physical connection: %r' % e) + raise + finally: + self._mux_handler.notify_writer_done() + + def stop(self, close_code=common.STATUS_NORMAL_CLOSURE): + """Stops the writer thread.""" + + self._deque_condition.acquire() + self._stop_requested = True + self._close_code = close_code + self._deque_condition.notify() + self._deque_condition.release() + + +class _PhysicalConnectionReader(threading.Thread): + """A thread that is responsible for reading data from physical connection. + """ + + def __init__(self, mux_handler): + """Constructs an instance. + + Args: + mux_handler: _MuxHandler instance. + """ + + threading.Thread.__init__(self) + self._logger = util.get_class_logger(self) + self._mux_handler = mux_handler + self.setDaemon(True) + + def run(self): + while True: + try: + physical_stream = self._mux_handler.physical_stream + message = physical_stream.receive_message() + if message is None: + break + # Below happens only when a data message is received. + opcode = physical_stream.get_last_received_opcode() + if opcode != common.OPCODE_BINARY: + self._mux_handler.fail_physical_connection( + _DROP_CODE_INVALID_ENCAPSULATING_MESSAGE, + 'Received a text message on physical connection') + break + + except ConnectionTerminatedException, e: + self._logger.debug('%s', e) + break + + try: + self._mux_handler.dispatch_message(message) + except PhysicalConnectionError, e: + self._mux_handler.fail_physical_connection( + e.drop_code, e.message) + break + except LogicalChannelError, e: + self._mux_handler.fail_logical_channel( + e.channel_id, e.drop_code, e.message) + except Exception, e: + self._logger.debug(traceback.format_exc()) + break + + self._mux_handler.notify_reader_done() + + +class _Worker(threading.Thread): + """A thread that is responsible for running the corresponding application + handler. + """ + + def __init__(self, mux_handler, request): + """Constructs an instance. + + Args: + mux_handler: _MuxHandler instance. + request: _LogicalRequest instance. + """ + + threading.Thread.__init__(self) + self._logger = util.get_class_logger(self) + self._mux_handler = mux_handler + self._request = request + self.setDaemon(True) + + def run(self): + self._logger.debug('Logical channel worker started. (id=%d)' % + self._request.channel_id) + try: + # Non-critical exceptions will be handled by dispatcher. + self._mux_handler.dispatcher.transfer_data(self._request) + except LogicalChannelError, e: + self._mux_handler.fail_logical_channel( + e.channel_id, e.drop_code, e.message) + finally: + self._mux_handler.notify_worker_done(self._request.channel_id) + + +class _MuxHandshaker(hybi.Handshaker): + """Opening handshake processor for multiplexing.""" + + _DUMMY_WEBSOCKET_KEY = 'dGhlIHNhbXBsZSBub25jZQ==' + + def __init__(self, request, dispatcher, send_quota, receive_quota): + """Constructs an instance. + Args: + request: _LogicalRequest instance. + dispatcher: Dispatcher instance (dispatch.Dispatcher). + send_quota: Initial send quota. + receive_quota: Initial receive quota. + """ + + hybi.Handshaker.__init__(self, request, dispatcher) + self._send_quota = send_quota + self._receive_quota = receive_quota + + # Append headers which should not be included in handshake field of + # AddChannelRequest. + # TODO(bashi): Make sure whether we should raise exception when + # these headers are included already. + request.headers_in[common.UPGRADE_HEADER] = ( + common.WEBSOCKET_UPGRADE_TYPE) + request.headers_in[common.SEC_WEBSOCKET_VERSION_HEADER] = ( + str(common.VERSION_HYBI_LATEST)) + request.headers_in[common.SEC_WEBSOCKET_KEY_HEADER] = ( + self._DUMMY_WEBSOCKET_KEY) + + def _create_stream(self, stream_options): + """Override hybi.Handshaker._create_stream.""" + + self._logger.debug('Creating logical stream for %d' % + self._request.channel_id) + return _LogicalStream( + self._request, stream_options, self._send_quota, + self._receive_quota) + + def _create_handshake_response(self, accept): + """Override hybi._create_handshake_response.""" + + response = [] + + response.append('HTTP/1.1 101 Switching Protocols\r\n') + + # Upgrade and Sec-WebSocket-Accept should be excluded. + response.append('%s: %s\r\n' % ( + common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE)) + if self._request.ws_protocol is not None: + response.append('%s: %s\r\n' % ( + common.SEC_WEBSOCKET_PROTOCOL_HEADER, + self._request.ws_protocol)) + if (self._request.ws_extensions is not None and + len(self._request.ws_extensions) != 0): + response.append('%s: %s\r\n' % ( + common.SEC_WEBSOCKET_EXTENSIONS_HEADER, + common.format_extensions(self._request.ws_extensions))) + response.append('\r\n') + + return ''.join(response) + + def _send_handshake(self, accept): + """Override hybi.Handshaker._send_handshake.""" + + # Don't send handshake response for the default channel + if self._request.channel_id == _DEFAULT_CHANNEL_ID: + return + + handshake_response = self._create_handshake_response(accept) + frame_data = _create_add_channel_response( + self._request.channel_id, + handshake_response) + self._logger.debug('Sending handshake response for %d: %r' % + (self._request.channel_id, frame_data)) + self._request.connection.write_control_data(frame_data) + + +class _LogicalChannelData(object): + """A structure that holds information about logical channel. + """ + + def __init__(self, request, worker): + self.request = request + self.worker = worker + self.drop_code = _DROP_CODE_NORMAL_CLOSURE + self.drop_message = '' + + +class _HandshakeDeltaBase(object): + """A class that holds information for delta-encoded handshake.""" + + def __init__(self, headers): + self._headers = headers + + def create_headers(self, delta=None): + """Creates request headers for an AddChannelRequest that has + delta-encoded handshake. + + Args: + delta: headers should be overridden. + """ + + headers = copy.copy(self._headers) + if delta: + for key, value in delta.items(): + # The spec requires that a header with an empty value is + # removed from the delta base. + if len(value) == 0 and headers.has_key(key): + del headers[key] + else: + headers[key] = value + return headers + + +class _MuxHandler(object): + """Multiplexing handler. When a handler starts, it launches three + threads; the reader thread, the writer thread, and a worker thread. + + The reader thread reads data from the physical stream, i.e., the + ws_stream object of the underlying websocket connection. The reader + thread interprets multiplexed frames and dispatches them to logical + channels. Methods of this class are mostly called by the reader thread. + + The writer thread sends multiplexed frames which are created by + logical channels via the physical connection. + + The worker thread launched at the starting point handles the + "Implicitly Opened Connection". If multiplexing handler receives + an AddChannelRequest and accepts it, the handler will launch a new worker + thread and dispatch the request to it. + """ + + def __init__(self, request, dispatcher): + """Constructs an instance. + + Args: + request: mod_python request of the physical connection. + dispatcher: Dispatcher instance (dispatch.Dispatcher). + """ + + self.original_request = request + self.dispatcher = dispatcher + self.physical_connection = request.connection + self.physical_stream = request.ws_stream + self._logger = util.get_class_logger(self) + self._logical_channels = {} + self._logical_channels_condition = threading.Condition() + # Holds client's initial quota + self._channel_slots = collections.deque() + self._handshake_base = None + self._worker_done_notify_received = False + self._reader = None + self._writer = None + + def start(self): + """Starts the handler. + + Raises: + MuxUnexpectedException: when the handler already started, or when + opening handshake of the default channel fails. + """ + + if self._reader or self._writer: + raise MuxUnexpectedException('MuxHandler already started') + + self._reader = _PhysicalConnectionReader(self) + self._writer = _PhysicalConnectionWriter(self) + self._reader.start() + self._writer.start() + + # Create "Implicitly Opened Connection". + logical_connection = _LogicalConnection(self, _DEFAULT_CHANNEL_ID) + headers = copy.copy(self.original_request.headers_in) + # Add extensions for logical channel. + headers[common.SEC_WEBSOCKET_EXTENSIONS_HEADER] = ( + common.format_extensions( + self.original_request.mux_processor.extensions())) + self._handshake_base = _HandshakeDeltaBase(headers) + logical_request = _LogicalRequest( + _DEFAULT_CHANNEL_ID, + self.original_request.method, + self.original_request.uri, + self.original_request.protocol, + self._handshake_base.create_headers(), + logical_connection) + # Client's send quota for the implicitly opened connection is zero, + # but we will send FlowControl later so set the initial quota to + # _INITIAL_QUOTA_FOR_CLIENT. + self._channel_slots.append(_INITIAL_QUOTA_FOR_CLIENT) + send_quota = self.original_request.mux_processor.quota() + if not self._do_handshake_for_logical_request( + logical_request, send_quota=send_quota): + raise MuxUnexpectedException( + 'Failed handshake on the default channel id') + self._add_logical_channel(logical_request) + + # Send FlowControl for the implicitly opened connection. + frame_data = _create_flow_control(_DEFAULT_CHANNEL_ID, + _INITIAL_QUOTA_FOR_CLIENT) + logical_request.connection.write_control_data(frame_data) + + def add_channel_slots(self, slots, send_quota): + """Adds channel slots. + + Args: + slots: number of slots to be added. + send_quota: initial send quota for slots. + """ + + self._channel_slots.extend([send_quota] * slots) + # Send NewChannelSlot to client. + frame_data = _create_new_channel_slot(slots, send_quota) + self.send_control_data(frame_data) + + def wait_until_done(self, timeout=None): + """Waits until all workers are done. Returns False when timeout has + occurred. Returns True on success. + + Args: + timeout: timeout in sec. + """ + + self._logical_channels_condition.acquire() + try: + while len(self._logical_channels) > 0: + self._logger.debug('Waiting workers(%d)...' % + len(self._logical_channels)) + self._worker_done_notify_received = False + self._logical_channels_condition.wait(timeout) + if not self._worker_done_notify_received: + self._logger.debug('Waiting worker(s) timed out') + return False + finally: + self._logical_channels_condition.release() + + # Flush pending outgoing data + self._writer.stop() + self._writer.join() + + return True + + def notify_write_data_done(self, channel_id): + """Called by the writer thread when a write operation has done. + + Args: + channel_id: objective channel id. + """ + + try: + self._logical_channels_condition.acquire() + if channel_id in self._logical_channels: + channel_data = self._logical_channels[channel_id] + channel_data.request.connection.on_write_data_done() + else: + self._logger.debug('Seems that logical channel for %d has gone' + % channel_id) + finally: + self._logical_channels_condition.release() + + def send_control_data(self, data): + """Sends data via the control channel. + + Args: + data: data to be sent. + """ + + self._writer.put_outgoing_data(_OutgoingData( + channel_id=_CONTROL_CHANNEL_ID, data=data)) + + def send_data(self, channel_id, data): + """Sends data via given logical channel. This method is called by + worker threads. + + Args: + channel_id: objective channel id. + data: data to be sent. + """ + + self._writer.put_outgoing_data(_OutgoingData( + channel_id=channel_id, data=data)) + + def _send_drop_channel(self, channel_id, code=None, message=''): + frame_data = _create_drop_channel(channel_id, code, message) + self._logger.debug( + 'Sending drop channel for channel id %d' % channel_id) + self.send_control_data(frame_data) + + def _send_error_add_channel_response(self, channel_id, status=None): + if status is None: + status = common.HTTP_STATUS_BAD_REQUEST + + if status in _HTTP_BAD_RESPONSE_MESSAGES: + message = _HTTP_BAD_RESPONSE_MESSAGES[status] + else: + self._logger.debug('Response message for %d is not found' % status) + message = '???' + + response = 'HTTP/1.1 %d %s\r\n\r\n' % (status, message) + frame_data = _create_add_channel_response(channel_id, + encoded_handshake=response, + encoding=0, rejected=True) + self.send_control_data(frame_data) + + def _create_logical_request(self, block): + if block.channel_id == _CONTROL_CHANNEL_ID: + # TODO(bashi): Raise PhysicalConnectionError with code 2006 + # instead of MuxUnexpectedException. + raise MuxUnexpectedException( + 'Received the control channel id (0) as objective channel ' + 'id for AddChannel') + + if block.encoding > _HANDSHAKE_ENCODING_DELTA: + raise PhysicalConnectionError( + _DROP_CODE_UNKNOWN_REQUEST_ENCODING) + + method, path, version, headers = _parse_request_text( + block.encoded_handshake) + if block.encoding == _HANDSHAKE_ENCODING_DELTA: + headers = self._handshake_base.create_headers(headers) + + connection = _LogicalConnection(self, block.channel_id) + request = _LogicalRequest(block.channel_id, method, path, version, + headers, connection) + return request + + def _do_handshake_for_logical_request(self, request, send_quota=0): + try: + receive_quota = self._channel_slots.popleft() + except IndexError: + raise LogicalChannelError( + request.channel_id, _DROP_CODE_NEW_CHANNEL_SLOT_VIOLATION) + + handshaker = _MuxHandshaker(request, self.dispatcher, + send_quota, receive_quota) + try: + handshaker.do_handshake() + except handshake.VersionException, e: + self._logger.info('%s', e) + self._send_error_add_channel_response( + request.channel_id, status=common.HTTP_STATUS_BAD_REQUEST) + return False + except handshake.HandshakeException, e: + # TODO(bashi): Should we _Fail the Logical Channel_ with 3001 + # instead? + self._logger.info('%s', e) + self._send_error_add_channel_response(request.channel_id, + status=e.status) + return False + except handshake.AbortedByUserException, e: + self._logger.info('%s', e) + self._send_error_add_channel_response(request.channel_id) + return False + + return True + + def _add_logical_channel(self, logical_request): + try: + self._logical_channels_condition.acquire() + if logical_request.channel_id in self._logical_channels: + self._logger.debug('Channel id %d already exists' % + logical_request.channel_id) + raise PhysicalConnectionError( + _DROP_CODE_CHANNEL_ALREADY_EXISTS, + 'Channel id %d already exists' % + logical_request.channel_id) + worker = _Worker(self, logical_request) + channel_data = _LogicalChannelData(logical_request, worker) + self._logical_channels[logical_request.channel_id] = channel_data + worker.start() + finally: + self._logical_channels_condition.release() + + def _process_add_channel_request(self, block): + try: + logical_request = self._create_logical_request(block) + except ValueError, e: + self._logger.debug('Failed to create logical request: %r' % e) + self._send_error_add_channel_response( + block.channel_id, status=common.HTTP_STATUS_BAD_REQUEST) + return + if self._do_handshake_for_logical_request(logical_request): + if block.encoding == _HANDSHAKE_ENCODING_IDENTITY: + # Update handshake base. + # TODO(bashi): Make sure this is the right place to update + # handshake base. + self._handshake_base = _HandshakeDeltaBase( + logical_request.headers_in) + self._add_logical_channel(logical_request) + else: + self._send_error_add_channel_response( + block.channel_id, status=common.HTTP_STATUS_BAD_REQUEST) + + def _process_flow_control(self, block): + try: + self._logical_channels_condition.acquire() + if not block.channel_id in self._logical_channels: + return + channel_data = self._logical_channels[block.channel_id] + channel_data.request.ws_stream.replenish_send_quota( + block.send_quota) + finally: + self._logical_channels_condition.release() + + def _process_drop_channel(self, block): + self._logger.debug( + 'DropChannel received for %d: code=%r, reason=%r' % + (block.channel_id, block.drop_code, block.drop_message)) + try: + self._logical_channels_condition.acquire() + if not block.channel_id in self._logical_channels: + return + channel_data = self._logical_channels[block.channel_id] + channel_data.drop_code = _DROP_CODE_ACKNOWLEDGED + + # Close the logical channel + channel_data.request.connection.set_read_state( + _LogicalConnection.STATE_TERMINATED) + channel_data.request.ws_stream.stop_sending() + finally: + self._logical_channels_condition.release() + + def _process_control_blocks(self, parser): + for control_block in parser.read_control_blocks(): + opcode = control_block.opcode + self._logger.debug('control block received, opcode: %d' % opcode) + if opcode == _MUX_OPCODE_ADD_CHANNEL_REQUEST: + self._process_add_channel_request(control_block) + elif opcode == _MUX_OPCODE_ADD_CHANNEL_RESPONSE: + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Received AddChannelResponse') + elif opcode == _MUX_OPCODE_FLOW_CONTROL: + self._process_flow_control(control_block) + elif opcode == _MUX_OPCODE_DROP_CHANNEL: + self._process_drop_channel(control_block) + elif opcode == _MUX_OPCODE_NEW_CHANNEL_SLOT: + raise PhysicalConnectionError( + _DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + 'Received NewChannelSlot') + else: + raise MuxUnexpectedException( + 'Unexpected opcode %r' % opcode) + + def _process_logical_frame(self, channel_id, parser): + self._logger.debug('Received a frame. channel id=%d' % channel_id) + try: + self._logical_channels_condition.acquire() + if not channel_id in self._logical_channels: + # We must ignore the message for an inactive channel. + return + channel_data = self._logical_channels[channel_id] + fin, rsv1, rsv2, rsv3, opcode, payload = parser.read_inner_frame() + consuming_byte = len(payload) + if opcode != common.OPCODE_CONTINUATION: + consuming_byte += 1 + if not channel_data.request.ws_stream.consume_receive_quota( + consuming_byte): + # The client violates quota. Close logical channel. + raise LogicalChannelError( + channel_id, _DROP_CODE_SEND_QUOTA_VIOLATION) + header = create_header(opcode, len(payload), fin, rsv1, rsv2, rsv3, + mask=False) + frame_data = header + payload + channel_data.request.connection.append_frame_data(frame_data) + finally: + self._logical_channels_condition.release() + + def dispatch_message(self, message): + """Dispatches message. The reader thread calls this method. + + Args: + message: a message that contains encapsulated frame. + Raises: + PhysicalConnectionError: if the message contains physical + connection level errors. + LogicalChannelError: if the message contains logical channel + level errors. + """ + + parser = _MuxFramePayloadParser(message) + try: + channel_id = parser.read_channel_id() + except ValueError, e: + raise PhysicalConnectionError(_DROP_CODE_CHANNEL_ID_TRUNCATED) + if channel_id == _CONTROL_CHANNEL_ID: + self._process_control_blocks(parser) + else: + self._process_logical_frame(channel_id, parser) + + def notify_worker_done(self, channel_id): + """Called when a worker has finished. + + Args: + channel_id: channel id corresponded with the worker. + """ + + self._logger.debug('Worker for channel id %d terminated' % channel_id) + try: + self._logical_channels_condition.acquire() + if not channel_id in self._logical_channels: + raise MuxUnexpectedException( + 'Channel id %d not found' % channel_id) + channel_data = self._logical_channels.pop(channel_id) + finally: + self._worker_done_notify_received = True + self._logical_channels_condition.notify() + self._logical_channels_condition.release() + + if not channel_data.request.server_terminated: + self._send_drop_channel( + channel_id, code=channel_data.drop_code, + message=channel_data.drop_message) + + def notify_reader_done(self): + """This method is called by the reader thread when the reader has + finished. + """ + + self._logger.debug( + 'Termiating all logical connections waiting for incoming data ' + '...') + self._logical_channels_condition.acquire() + for channel_data in self._logical_channels.values(): + try: + channel_data.request.connection.set_read_state( + _LogicalConnection.STATE_TERMINATED) + except Exception: + self._logger.debug(traceback.format_exc()) + self._logical_channels_condition.release() + + def notify_writer_done(self): + """This method is called by the writer thread when the writer has + finished. + """ + + self._logger.debug( + 'Termiating all logical connections waiting for write ' + 'completion ...') + self._logical_channels_condition.acquire() + for channel_data in self._logical_channels.values(): + try: + channel_data.request.connection.on_writer_done() + except Exception: + self._logger.debug(traceback.format_exc()) + self._logical_channels_condition.release() + + def fail_physical_connection(self, code, message): + """Fail the physical connection. + + Args: + code: drop reason code. + message: drop message. + """ + + self._logger.debug('Failing the physical connection...') + self._send_drop_channel(_CONTROL_CHANNEL_ID, code, message) + self._writer.stop(common.STATUS_INTERNAL_ENDPOINT_ERROR) + + def fail_logical_channel(self, channel_id, code, message): + """Fail a logical channel. + + Args: + channel_id: channel id. + code: drop reason code. + message: drop message. + """ + + self._logger.debug('Failing logical channel %d...' % channel_id) + try: + self._logical_channels_condition.acquire() + if channel_id in self._logical_channels: + channel_data = self._logical_channels[channel_id] + # Close the logical channel. notify_worker_done() will be + # called later and it will send DropChannel. + channel_data.drop_code = code + channel_data.drop_message = message + + channel_data.request.connection.set_read_state( + _LogicalConnection.STATE_TERMINATED) + channel_data.request.ws_stream.stop_sending() + else: + self._send_drop_channel(channel_id, code, message) + finally: + self._logical_channels_condition.release() + + +def use_mux(request): + return hasattr(request, 'mux_processor') and ( + request.mux_processor.is_active()) + + +def start(request, dispatcher): + mux_handler = _MuxHandler(request, dispatcher) + mux_handler.start() + + mux_handler.add_channel_slots(_INITIAL_NUMBER_OF_CHANNEL_SLOTS, + _INITIAL_QUOTA_FOR_CLIENT) + + mux_handler.wait_until_done() + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/standalone.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/standalone.py new file mode 100755 index 000000000..24c299eaf --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/standalone.py @@ -0,0 +1,1193 @@ +#!/usr/bin/env python +# +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Standalone WebSocket server. + +Use this file to launch pywebsocket without Apache HTTP Server. + + +BASIC USAGE +=========== + +Go to the src directory and run + + $ python mod_pywebsocket/standalone.py [-p <ws_port>] + [-w <websock_handlers>] + [-d <document_root>] + +<ws_port> is the port number to use for ws:// connection. + +<document_root> is the path to the root directory of HTML files. + +<websock_handlers> is the path to the root directory of WebSocket handlers. +If not specified, <document_root> will be used. See __init__.py (or +run $ pydoc mod_pywebsocket) for how to write WebSocket handlers. + +For more detail and other options, run + + $ python mod_pywebsocket/standalone.py --help + +or see _build_option_parser method below. + +For trouble shooting, adding "--log_level debug" might help you. + + +TRY DEMO +======== + +Go to the src directory and run standalone.py with -d option to set the +document root to the directory containing example HTMLs and handlers like this: + + $ cd src + $ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example + +to launch pywebsocket with the sample handler and html on port 80. Open +http://localhost/console.html, click the connect button, type something into +the text box next to the send button and click the send button. If everything +is working, you'll see the message you typed echoed by the server. + + +USING TLS +========= + +To run the standalone server with TLS support, run it with -t, -k, and -c +options. When TLS is enabled, the standalone server accepts only TLS connection. + +Note that when ssl module is used and the key/cert location is incorrect, +TLS connection silently fails while pyOpenSSL fails on startup. + +Example: + + $ PYTHONPATH=. python mod_pywebsocket/standalone.py \ + -d example \ + -p 10443 \ + -t \ + -c ../test/cert/cert.pem \ + -k ../test/cert/key.pem \ + +Note that when passing a relative path to -c and -k option, it will be resolved +using the document root directory as the base. + + +USING CLIENT AUTHENTICATION +=========================== + +To run the standalone server with TLS client authentication support, run it with +--tls-client-auth and --tls-client-ca options in addition to ones required for +TLS support. + +Example: + + $ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example -p 10443 -t \ + -c ../test/cert/cert.pem -k ../test/cert/key.pem \ + --tls-client-auth \ + --tls-client-ca=../test/cert/cacert.pem + +Note that when passing a relative path to --tls-client-ca option, it will be +resolved using the document root directory as the base. + + +CONFIGURATION FILE +================== + +You can also write a configuration file and use it by specifying the path to +the configuration file by --config option. Please write a configuration file +following the documentation of the Python ConfigParser library. Name of each +entry must be the long version argument name. E.g. to set log level to debug, +add the following line: + +log_level=debug + +For options which doesn't take value, please add some fake value. E.g. for +--tls option, add the following line: + +tls=True + +Note that tls will be enabled even if you write tls=False as the value part is +fake. + +When both a command line argument and a configuration file entry are set for +the same configuration item, the command line value will override one in the +configuration file. + + +THREADING +========= + +This server is derived from SocketServer.ThreadingMixIn. Hence a thread is +used for each request. + + +SECURITY WARNING +================ + +This uses CGIHTTPServer and CGIHTTPServer is not secure. +It may execute arbitrary Python code or external programs. It should not be +used outside a firewall. +""" + +import BaseHTTPServer +import CGIHTTPServer +import SimpleHTTPServer +import SocketServer +import ConfigParser +import base64 +import httplib +import logging +import logging.handlers +import optparse +import os +import re +import select +import socket +import sys +import threading +import time + +from mod_pywebsocket import common +from mod_pywebsocket import dispatch +from mod_pywebsocket import handshake +from mod_pywebsocket import http_header_util +from mod_pywebsocket import memorizingfile +from mod_pywebsocket import util +from mod_pywebsocket.xhr_benchmark_handler import XHRBenchmarkHandler + + +_DEFAULT_LOG_MAX_BYTES = 1024 * 256 +_DEFAULT_LOG_BACKUP_COUNT = 5 + +_DEFAULT_REQUEST_QUEUE_SIZE = 128 + +# 1024 is practically large enough to contain WebSocket handshake lines. +_MAX_MEMORIZED_LINES = 1024 + +# Constants for the --tls_module flag. +_TLS_BY_STANDARD_MODULE = 'ssl' +_TLS_BY_PYOPENSSL = 'pyopenssl' + + +class _StandaloneConnection(object): + """Mimic mod_python mp_conn.""" + + def __init__(self, request_handler): + """Construct an instance. + + Args: + request_handler: A WebSocketRequestHandler instance. + """ + + self._request_handler = request_handler + + def get_local_addr(self): + """Getter to mimic mp_conn.local_addr.""" + + return (self._request_handler.server.server_name, + self._request_handler.server.server_port) + local_addr = property(get_local_addr) + + def get_remote_addr(self): + """Getter to mimic mp_conn.remote_addr. + + Setting the property in __init__ won't work because the request + handler is not initialized yet there.""" + + return self._request_handler.client_address + remote_addr = property(get_remote_addr) + + def write(self, data): + """Mimic mp_conn.write().""" + + return self._request_handler.wfile.write(data) + + def read(self, length): + """Mimic mp_conn.read().""" + + return self._request_handler.rfile.read(length) + + def get_memorized_lines(self): + """Get memorized lines.""" + + return self._request_handler.rfile.get_memorized_lines() + + +class _StandaloneRequest(object): + """Mimic mod_python request.""" + + def __init__(self, request_handler, use_tls): + """Construct an instance. + + Args: + request_handler: A WebSocketRequestHandler instance. + """ + + self._logger = util.get_class_logger(self) + + self._request_handler = request_handler + self.connection = _StandaloneConnection(request_handler) + self._use_tls = use_tls + self.headers_in = request_handler.headers + + def get_uri(self): + """Getter to mimic request.uri. + + This method returns the raw data at the Request-URI part of the + Request-Line, while the uri method on the request object of mod_python + returns the path portion after parsing the raw data. This behavior is + kept for compatibility. + """ + + return self._request_handler.path + uri = property(get_uri) + + def get_unparsed_uri(self): + """Getter to mimic request.unparsed_uri.""" + + return self._request_handler.path + unparsed_uri = property(get_unparsed_uri) + + def get_method(self): + """Getter to mimic request.method.""" + + return self._request_handler.command + method = property(get_method) + + def get_protocol(self): + """Getter to mimic request.protocol.""" + + return self._request_handler.request_version + protocol = property(get_protocol) + + def is_https(self): + """Mimic request.is_https().""" + + return self._use_tls + + +def _import_ssl(): + global ssl + try: + import ssl + return True + except ImportError: + return False + + +def _import_pyopenssl(): + global OpenSSL + try: + import OpenSSL.SSL + return True + except ImportError: + return False + + +class _StandaloneSSLConnection(object): + """A wrapper class for OpenSSL.SSL.Connection to + - provide makefile method which is not supported by the class + - tweak shutdown method since OpenSSL.SSL.Connection.shutdown doesn't + accept the "how" argument. + - convert SysCallError exceptions that its recv method may raise into a + return value of '', meaning EOF. We cannot overwrite the recv method on + self._connection since it's immutable. + """ + + _OVERRIDDEN_ATTRIBUTES = ['_connection', 'makefile', 'shutdown', 'recv'] + + def __init__(self, connection): + self._connection = connection + + def __getattribute__(self, name): + if name in _StandaloneSSLConnection._OVERRIDDEN_ATTRIBUTES: + return object.__getattribute__(self, name) + return self._connection.__getattribute__(name) + + def __setattr__(self, name, value): + if name in _StandaloneSSLConnection._OVERRIDDEN_ATTRIBUTES: + return object.__setattr__(self, name, value) + return self._connection.__setattr__(name, value) + + def makefile(self, mode='r', bufsize=-1): + return socket._fileobject(self, mode, bufsize) + + def shutdown(self, unused_how): + self._connection.shutdown() + + def recv(self, bufsize, flags=0): + if flags != 0: + raise ValueError('Non-zero flags not allowed') + + try: + return self._connection.recv(bufsize) + except OpenSSL.SSL.SysCallError, (err, message): + if err == -1: + # Suppress "unexpected EOF" exception. See the OpenSSL document + # for SSL_get_error. + return '' + raise + + +def _alias_handlers(dispatcher, websock_handlers_map_file): + """Set aliases specified in websock_handler_map_file in dispatcher. + + Args: + dispatcher: dispatch.Dispatcher instance + websock_handler_map_file: alias map file + """ + + fp = open(websock_handlers_map_file) + try: + for line in fp: + if line[0] == '#' or line.isspace(): + continue + m = re.match('(\S+)\s+(\S+)', line) + if not m: + logging.warning('Wrong format in map file:' + line) + continue + try: + dispatcher.add_resource_path_alias( + m.group(1), m.group(2)) + except dispatch.DispatchException, e: + logging.error(str(e)) + finally: + fp.close() + + +class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): + """HTTPServer specialized for WebSocket.""" + + # Overrides SocketServer.ThreadingMixIn.daemon_threads + daemon_threads = True + # Overrides BaseHTTPServer.HTTPServer.allow_reuse_address + allow_reuse_address = True + + def __init__(self, options): + """Override SocketServer.TCPServer.__init__ to set SSL enabled + socket object to self.socket before server_bind and server_activate, + if necessary. + """ + + # Share a Dispatcher among request handlers to save time for + # instantiation. Dispatcher can be shared because it is thread-safe. + options.dispatcher = dispatch.Dispatcher( + options.websock_handlers, + options.scan_dir, + options.allow_handlers_outside_root_dir) + if options.websock_handlers_map_file: + _alias_handlers(options.dispatcher, + options.websock_handlers_map_file) + warnings = options.dispatcher.source_warnings() + if warnings: + for warning in warnings: + logging.warning('Warning in source loading: %s' % warning) + + self._logger = util.get_class_logger(self) + + self.request_queue_size = options.request_queue_size + self.__ws_is_shut_down = threading.Event() + self.__ws_serving = False + + SocketServer.BaseServer.__init__( + self, (options.server_host, options.port), WebSocketRequestHandler) + + # Expose the options object to allow handler objects access it. We name + # it with websocket_ prefix to avoid conflict. + self.websocket_server_options = options + + self._create_sockets() + self.server_bind() + self.server_activate() + + def _create_sockets(self): + self.server_name, self.server_port = self.server_address + self._sockets = [] + if not self.server_name: + # On platforms that doesn't support IPv6, the first bind fails. + # On platforms that supports IPv6 + # - If it binds both IPv4 and IPv6 on call with AF_INET6, the + # first bind succeeds and the second fails (we'll see 'Address + # already in use' error). + # - If it binds only IPv6 on call with AF_INET6, both call are + # expected to succeed to listen both protocol. + addrinfo_array = [ + (socket.AF_INET6, socket.SOCK_STREAM, '', '', ''), + (socket.AF_INET, socket.SOCK_STREAM, '', '', '')] + else: + addrinfo_array = socket.getaddrinfo(self.server_name, + self.server_port, + socket.AF_UNSPEC, + socket.SOCK_STREAM, + socket.IPPROTO_TCP) + for addrinfo in addrinfo_array: + self._logger.info('Create socket on: %r', addrinfo) + family, socktype, proto, canonname, sockaddr = addrinfo + try: + socket_ = socket.socket(family, socktype) + except Exception, e: + self._logger.info('Skip by failure: %r', e) + continue + server_options = self.websocket_server_options + if server_options.use_tls: + # For the case of _HAS_OPEN_SSL, we do wrapper setup after + # accept. + if server_options.tls_module == _TLS_BY_STANDARD_MODULE: + if server_options.tls_client_auth: + if server_options.tls_client_cert_optional: + client_cert_ = ssl.CERT_OPTIONAL + else: + client_cert_ = ssl.CERT_REQUIRED + else: + client_cert_ = ssl.CERT_NONE + socket_ = ssl.wrap_socket(socket_, + keyfile=server_options.private_key, + certfile=server_options.certificate, + ssl_version=ssl.PROTOCOL_SSLv23, + ca_certs=server_options.tls_client_ca, + cert_reqs=client_cert_, + do_handshake_on_connect=False) + self._sockets.append((socket_, addrinfo)) + + def server_bind(self): + """Override SocketServer.TCPServer.server_bind to enable multiple + sockets bind. + """ + + failed_sockets = [] + + for socketinfo in self._sockets: + socket_, addrinfo = socketinfo + self._logger.info('Bind on: %r', addrinfo) + if self.allow_reuse_address: + socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + socket_.bind(self.server_address) + except Exception, e: + self._logger.info('Skip by failure: %r', e) + socket_.close() + failed_sockets.append(socketinfo) + if self.server_address[1] == 0: + # The operating system assigns the actual port number for port + # number 0. This case, the second and later sockets should use + # the same port number. Also self.server_port is rewritten + # because it is exported, and will be used by external code. + self.server_address = ( + self.server_name, socket_.getsockname()[1]) + self.server_port = self.server_address[1] + self._logger.info('Port %r is assigned', self.server_port) + + for socketinfo in failed_sockets: + self._sockets.remove(socketinfo) + + def server_activate(self): + """Override SocketServer.TCPServer.server_activate to enable multiple + sockets listen. + """ + + failed_sockets = [] + + for socketinfo in self._sockets: + socket_, addrinfo = socketinfo + self._logger.info('Listen on: %r', addrinfo) + try: + socket_.listen(self.request_queue_size) + except Exception, e: + self._logger.info('Skip by failure: %r', e) + socket_.close() + failed_sockets.append(socketinfo) + + for socketinfo in failed_sockets: + self._sockets.remove(socketinfo) + + if len(self._sockets) == 0: + self._logger.critical( + 'No sockets activated. Use info log level to see the reason.') + + def server_close(self): + """Override SocketServer.TCPServer.server_close to enable multiple + sockets close. + """ + + for socketinfo in self._sockets: + socket_, addrinfo = socketinfo + self._logger.info('Close on: %r', addrinfo) + socket_.close() + + def fileno(self): + """Override SocketServer.TCPServer.fileno.""" + + self._logger.critical('Not supported: fileno') + return self._sockets[0][0].fileno() + + def handle_error(self, request, client_address): + """Override SocketServer.handle_error.""" + + self._logger.error( + 'Exception in processing request from: %r\n%s', + client_address, + util.get_stack_trace()) + # Note: client_address is a tuple. + + def get_request(self): + """Override TCPServer.get_request to wrap OpenSSL.SSL.Connection + object with _StandaloneSSLConnection to provide makefile method. We + cannot substitute OpenSSL.SSL.Connection.makefile since it's readonly + attribute. + """ + + accepted_socket, client_address = self.socket.accept() + + server_options = self.websocket_server_options + if server_options.use_tls: + if server_options.tls_module == _TLS_BY_STANDARD_MODULE: + try: + accepted_socket.do_handshake() + except ssl.SSLError, e: + self._logger.debug('%r', e) + raise + + # Print cipher in use. Handshake is done on accept. + self._logger.debug('Cipher: %s', accepted_socket.cipher()) + self._logger.debug('Client cert: %r', + accepted_socket.getpeercert()) + elif server_options.tls_module == _TLS_BY_PYOPENSSL: + # We cannot print the cipher in use. pyOpenSSL doesn't provide + # any method to fetch that. + + ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) + ctx.use_privatekey_file(server_options.private_key) + ctx.use_certificate_file(server_options.certificate) + + def default_callback(conn, cert, errnum, errdepth, ok): + return ok == 1 + + # See the OpenSSL document for SSL_CTX_set_verify. + if server_options.tls_client_auth: + verify_mode = OpenSSL.SSL.VERIFY_PEER + if not server_options.tls_client_cert_optional: + verify_mode |= OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT + ctx.set_verify(verify_mode, default_callback) + ctx.load_verify_locations(server_options.tls_client_ca, + None) + else: + ctx.set_verify(OpenSSL.SSL.VERIFY_NONE, default_callback) + + accepted_socket = OpenSSL.SSL.Connection(ctx, accepted_socket) + accepted_socket.set_accept_state() + + # Convert SSL related error into socket.error so that + # SocketServer ignores them and keeps running. + # + # TODO(tyoshino): Convert all kinds of errors. + try: + accepted_socket.do_handshake() + except OpenSSL.SSL.Error, e: + # Set errno part to 1 (SSL_ERROR_SSL) like the ssl module + # does. + self._logger.debug('%r', e) + raise socket.error(1, '%r' % e) + cert = accepted_socket.get_peer_certificate() + if cert is not None: + self._logger.debug('Client cert subject: %r', + cert.get_subject().get_components()) + accepted_socket = _StandaloneSSLConnection(accepted_socket) + else: + raise ValueError('No TLS support module is available') + + return accepted_socket, client_address + + def serve_forever(self, poll_interval=0.5): + """Override SocketServer.BaseServer.serve_forever.""" + + self.__ws_serving = True + self.__ws_is_shut_down.clear() + handle_request = self.handle_request + if hasattr(self, '_handle_request_noblock'): + handle_request = self._handle_request_noblock + else: + self._logger.warning('Fallback to blocking request handler') + try: + while self.__ws_serving: + r, w, e = select.select( + [socket_[0] for socket_ in self._sockets], + [], [], poll_interval) + for socket_ in r: + self.socket = socket_ + handle_request() + self.socket = None + finally: + self.__ws_is_shut_down.set() + + def shutdown(self): + """Override SocketServer.BaseServer.shutdown.""" + + self.__ws_serving = False + self.__ws_is_shut_down.wait() + + +class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): + """CGIHTTPRequestHandler specialized for WebSocket.""" + + # Use httplib.HTTPMessage instead of mimetools.Message. + MessageClass = httplib.HTTPMessage + + protocol_version = "HTTP/1.1" + + def setup(self): + """Override SocketServer.StreamRequestHandler.setup to wrap rfile + with MemorizingFile. + + This method will be called by BaseRequestHandler's constructor + before calling BaseHTTPRequestHandler.handle. + BaseHTTPRequestHandler.handle will call + BaseHTTPRequestHandler.handle_one_request and it will call + WebSocketRequestHandler.parse_request. + """ + + # Call superclass's setup to prepare rfile, wfile, etc. See setup + # definition on the root class SocketServer.StreamRequestHandler to + # understand what this does. + CGIHTTPServer.CGIHTTPRequestHandler.setup(self) + + self.rfile = memorizingfile.MemorizingFile( + self.rfile, + max_memorized_lines=_MAX_MEMORIZED_LINES) + + def __init__(self, request, client_address, server): + self._logger = util.get_class_logger(self) + + self._options = server.websocket_server_options + + # Overrides CGIHTTPServerRequestHandler.cgi_directories. + self.cgi_directories = self._options.cgi_directories + # Replace CGIHTTPRequestHandler.is_executable method. + if self._options.is_executable_method is not None: + self.is_executable = self._options.is_executable_method + + # This actually calls BaseRequestHandler.__init__. + CGIHTTPServer.CGIHTTPRequestHandler.__init__( + self, request, client_address, server) + + def parse_request(self): + """Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request. + + Return True to continue processing for HTTP(S), False otherwise. + + See BaseHTTPRequestHandler.handle_one_request method which calls + this method to understand how the return value will be handled. + """ + + # We hook parse_request method, but also call the original + # CGIHTTPRequestHandler.parse_request since when we return False, + # CGIHTTPRequestHandler.handle_one_request continues processing and + # it needs variables set by CGIHTTPRequestHandler.parse_request. + # + # Variables set by this method will be also used by WebSocket request + # handling (self.path, self.command, self.requestline, etc. See also + # how _StandaloneRequest's members are implemented using these + # attributes). + if not CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self): + return False + + if self.command == "CONNECT": + self.send_response(200, "Connected") + self.send_header("Connection", "keep-alive") + self.end_headers() + return False + + if self._options.use_basic_auth: + auth = self.headers.getheader('Authorization') + if auth != self._options.basic_auth_credential: + self.send_response(401) + self.send_header('WWW-Authenticate', + 'Basic realm="Pywebsocket"') + self.end_headers() + self._logger.info('Request basic authentication') + return False + + host, port, resource = http_header_util.parse_uri(self.path) + + # Special paths for XMLHttpRequest benchmark + xhr_benchmark_helper_prefix = '/073be001e10950692ccbf3a2ad21c245' + if resource == (xhr_benchmark_helper_prefix + '_send'): + xhr_benchmark_handler = XHRBenchmarkHandler( + self.headers, self.rfile, self.wfile) + xhr_benchmark_handler.do_send() + return False + if resource == (xhr_benchmark_helper_prefix + '_receive'): + xhr_benchmark_handler = XHRBenchmarkHandler( + self.headers, self.rfile, self.wfile) + xhr_benchmark_handler.do_receive() + return False + + if resource is None: + self._logger.info('Invalid URI: %r', self.path) + self._logger.info('Fallback to CGIHTTPRequestHandler') + return True + server_options = self.server.websocket_server_options + if host is not None: + validation_host = server_options.validation_host + if validation_host is not None and host != validation_host: + self._logger.info('Invalid host: %r (expected: %r)', + host, + validation_host) + self._logger.info('Fallback to CGIHTTPRequestHandler') + return True + if port is not None: + validation_port = server_options.validation_port + if validation_port is not None and port != validation_port: + self._logger.info('Invalid port: %r (expected: %r)', + port, + validation_port) + self._logger.info('Fallback to CGIHTTPRequestHandler') + return True + self.path = resource + + request = _StandaloneRequest(self, self._options.use_tls) + + try: + # Fallback to default http handler for request paths for which + # we don't have request handlers. + if not self._options.dispatcher.get_handler_suite(self.path): + self._logger.info('No handler for resource: %r', + self.path) + self._logger.info('Fallback to CGIHTTPRequestHandler') + return True + except dispatch.DispatchException, e: + self._logger.info('Dispatch failed for error: %s', e) + self.send_error(e.status) + return False + + # If any Exceptions without except clause setup (including + # DispatchException) is raised below this point, it will be caught + # and logged by WebSocketServer. + + try: + try: + handshake.do_handshake( + request, + self._options.dispatcher, + allowDraft75=self._options.allow_draft75, + strict=self._options.strict) + except handshake.VersionException, e: + self._logger.info('Handshake failed for version error: %s', e) + self.send_response(common.HTTP_STATUS_BAD_REQUEST) + self.send_header(common.SEC_WEBSOCKET_VERSION_HEADER, + e.supported_versions) + self.end_headers() + return False + except handshake.HandshakeException, e: + # Handshake for ws(s) failed. + self._logger.info('Handshake failed for error: %s', e) + self.send_error(e.status) + return False + + request._dispatcher = self._options.dispatcher + self._options.dispatcher.transfer_data(request) + except handshake.AbortedByUserException, e: + self._logger.info('Aborted: %s', e) + return False + + def log_request(self, code='-', size='-'): + """Override BaseHTTPServer.log_request.""" + + self._logger.info('"%s" %s %s', + self.requestline, str(code), str(size)) + + def log_error(self, *args): + """Override BaseHTTPServer.log_error.""" + + # Despite the name, this method is for warnings than for errors. + # For example, HTTP status code is logged by this method. + self._logger.warning('%s - %s', + self.address_string(), + args[0] % args[1:]) + + def is_cgi(self): + """Test whether self.path corresponds to a CGI script. + + Add extra check that self.path doesn't contains .. + Also check if the file is a executable file or not. + If the file is not executable, it is handled as static file or dir + rather than a CGI script. + """ + + if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self): + if '..' in self.path: + return False + # strip query parameter from request path + resource_name = self.path.split('?', 2)[0] + # convert resource_name into real path name in filesystem. + scriptfile = self.translate_path(resource_name) + if not os.path.isfile(scriptfile): + return False + if not self.is_executable(scriptfile): + return False + return True + return False + + +def _get_logger_from_class(c): + return logging.getLogger('%s.%s' % (c.__module__, c.__name__)) + + +def _configure_logging(options): + logging.addLevelName(common.LOGLEVEL_FINE, 'FINE') + + logger = logging.getLogger() + logger.setLevel(logging.getLevelName(options.log_level.upper())) + if options.log_file: + handler = logging.handlers.RotatingFileHandler( + options.log_file, 'a', options.log_max, options.log_count) + else: + handler = logging.StreamHandler() + formatter = logging.Formatter( + '[%(asctime)s] [%(levelname)s] %(name)s: %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + + deflate_log_level_name = logging.getLevelName( + options.deflate_log_level.upper()) + _get_logger_from_class(util._Deflater).setLevel( + deflate_log_level_name) + _get_logger_from_class(util._Inflater).setLevel( + deflate_log_level_name) + + +def _build_option_parser(): + parser = optparse.OptionParser() + + parser.add_option('--config', dest='config_file', type='string', + default=None, + help=('Path to configuration file. See the file comment ' + 'at the top of this file for the configuration ' + 'file format')) + parser.add_option('-H', '--server-host', '--server_host', + dest='server_host', + default='', + help='server hostname to listen to') + parser.add_option('-V', '--validation-host', '--validation_host', + dest='validation_host', + default=None, + help='server hostname to validate in absolute path.') + parser.add_option('-p', '--port', dest='port', type='int', + default=common.DEFAULT_WEB_SOCKET_PORT, + help='port to listen to') + parser.add_option('-P', '--validation-port', '--validation_port', + dest='validation_port', type='int', + default=None, + help='server port to validate in absolute path.') + parser.add_option('-w', '--websock-handlers', '--websock_handlers', + dest='websock_handlers', + default='.', + help=('The root directory of WebSocket handler files. ' + 'If the path is relative, --document-root is used ' + 'as the base.')) + parser.add_option('-m', '--websock-handlers-map-file', + '--websock_handlers_map_file', + dest='websock_handlers_map_file', + default=None, + help=('WebSocket handlers map file. ' + 'Each line consists of alias_resource_path and ' + 'existing_resource_path, separated by spaces.')) + parser.add_option('-s', '--scan-dir', '--scan_dir', dest='scan_dir', + default=None, + help=('Must be a directory under --websock-handlers. ' + 'Only handlers under this directory are scanned ' + 'and registered to the server. ' + 'Useful for saving scan time when the handler ' + 'root directory contains lots of files that are ' + 'not handler file or are handler files but you ' + 'don\'t want them to be registered. ')) + parser.add_option('--allow-handlers-outside-root-dir', + '--allow_handlers_outside_root_dir', + dest='allow_handlers_outside_root_dir', + action='store_true', + default=False, + help=('Scans WebSocket handlers even if their canonical ' + 'path is not under --websock-handlers.')) + parser.add_option('-d', '--document-root', '--document_root', + dest='document_root', default='.', + help='Document root directory.') + parser.add_option('-x', '--cgi-paths', '--cgi_paths', dest='cgi_paths', + default=None, + help=('CGI paths relative to document_root.' + 'Comma-separated. (e.g -x /cgi,/htbin) ' + 'Files under document_root/cgi_path are handled ' + 'as CGI programs. Must be executable.')) + parser.add_option('-t', '--tls', dest='use_tls', action='store_true', + default=False, help='use TLS (wss://)') + parser.add_option('--tls-module', '--tls_module', dest='tls_module', + type='choice', + choices = [_TLS_BY_STANDARD_MODULE, _TLS_BY_PYOPENSSL], + help='Use ssl module if "%s" is specified. ' + 'Use pyOpenSSL module if "%s" is specified' % + (_TLS_BY_STANDARD_MODULE, _TLS_BY_PYOPENSSL)) + parser.add_option('-k', '--private-key', '--private_key', + dest='private_key', + default='', help='TLS private key file.') + parser.add_option('-c', '--certificate', dest='certificate', + default='', help='TLS certificate file.') + parser.add_option('--tls-client-auth', dest='tls_client_auth', + action='store_true', default=False, + help='Requests TLS client auth on every connection.') + parser.add_option('--tls-client-cert-optional', + dest='tls_client_cert_optional', + action='store_true', default=False, + help=('Makes client certificate optional even though ' + 'TLS client auth is enabled.')) + parser.add_option('--tls-client-ca', dest='tls_client_ca', default='', + help=('Specifies a pem file which contains a set of ' + 'concatenated CA certificates which are used to ' + 'validate certificates passed from clients')) + parser.add_option('--basic-auth', dest='use_basic_auth', + action='store_true', default=False, + help='Requires Basic authentication.') + parser.add_option('--basic-auth-credential', + dest='basic_auth_credential', default='test:test', + help='Specifies the credential of basic authentication ' + 'by username:password pair (e.g. test:test).') + parser.add_option('-l', '--log-file', '--log_file', dest='log_file', + default='', help='Log file.') + # Custom log level: + # - FINE: Prints status of each frame processing step + parser.add_option('--log-level', '--log_level', type='choice', + dest='log_level', default='warn', + choices=['fine', + 'debug', 'info', 'warning', 'warn', 'error', + 'critical'], + help='Log level.') + parser.add_option('--deflate-log-level', '--deflate_log_level', + type='choice', + dest='deflate_log_level', default='warn', + choices=['debug', 'info', 'warning', 'warn', 'error', + 'critical'], + help='Log level for _Deflater and _Inflater.') + parser.add_option('--thread-monitor-interval-in-sec', + '--thread_monitor_interval_in_sec', + dest='thread_monitor_interval_in_sec', + type='int', default=-1, + help=('If positive integer is specified, run a thread ' + 'monitor to show the status of server threads ' + 'periodically in the specified inteval in ' + 'second. If non-positive integer is specified, ' + 'disable the thread monitor.')) + parser.add_option('--log-max', '--log_max', dest='log_max', type='int', + default=_DEFAULT_LOG_MAX_BYTES, + help='Log maximum bytes') + parser.add_option('--log-count', '--log_count', dest='log_count', + type='int', default=_DEFAULT_LOG_BACKUP_COUNT, + help='Log backup count') + parser.add_option('--allow-draft75', dest='allow_draft75', + action='store_true', default=False, + help='Obsolete option. Ignored.') + parser.add_option('--strict', dest='strict', action='store_true', + default=False, help='Obsolete option. Ignored.') + parser.add_option('-q', '--queue', dest='request_queue_size', type='int', + default=_DEFAULT_REQUEST_QUEUE_SIZE, + help='request queue size') + + return parser + + +class ThreadMonitor(threading.Thread): + daemon = True + + def __init__(self, interval_in_sec): + threading.Thread.__init__(self, name='ThreadMonitor') + + self._logger = util.get_class_logger(self) + + self._interval_in_sec = interval_in_sec + + def run(self): + while True: + thread_name_list = [] + for thread in threading.enumerate(): + thread_name_list.append(thread.name) + self._logger.info( + "%d active threads: %s", + threading.active_count(), + ', '.join(thread_name_list)) + time.sleep(self._interval_in_sec) + + +def _parse_args_and_config(args): + parser = _build_option_parser() + + # First, parse options without configuration file. + temporary_options, temporary_args = parser.parse_args(args=args) + if temporary_args: + logging.critical( + 'Unrecognized positional arguments: %r', temporary_args) + sys.exit(1) + + if temporary_options.config_file: + try: + config_fp = open(temporary_options.config_file, 'r') + except IOError, e: + logging.critical( + 'Failed to open configuration file %r: %r', + temporary_options.config_file, + e) + sys.exit(1) + + config_parser = ConfigParser.SafeConfigParser() + config_parser.readfp(config_fp) + config_fp.close() + + args_from_config = [] + for name, value in config_parser.items('pywebsocket'): + args_from_config.append('--' + name) + args_from_config.append(value) + if args is None: + args = args_from_config + else: + args = args_from_config + args + return parser.parse_args(args=args) + else: + return temporary_options, temporary_args + + +def _main(args=None): + """You can call this function from your own program, but please note that + this function has some side-effects that might affect your program. For + example, util.wrap_popen3_for_win use in this method replaces implementation + of os.popen3. + """ + + options, args = _parse_args_and_config(args=args) + + os.chdir(options.document_root) + + _configure_logging(options) + + if options.allow_draft75: + logging.warning('--allow_draft75 option is obsolete.') + + if options.strict: + logging.warning('--strict option is obsolete.') + + # TODO(tyoshino): Clean up initialization of CGI related values. Move some + # of code here to WebSocketRequestHandler class if it's better. + options.cgi_directories = [] + options.is_executable_method = None + if options.cgi_paths: + options.cgi_directories = options.cgi_paths.split(',') + if sys.platform in ('cygwin', 'win32'): + cygwin_path = None + # For Win32 Python, it is expected that CYGWIN_PATH + # is set to a directory of cygwin binaries. + # For example, websocket_server.py in Chromium sets CYGWIN_PATH to + # full path of third_party/cygwin/bin. + if 'CYGWIN_PATH' in os.environ: + cygwin_path = os.environ['CYGWIN_PATH'] + util.wrap_popen3_for_win(cygwin_path) + + def __check_script(scriptpath): + return util.get_script_interp(scriptpath, cygwin_path) + + options.is_executable_method = __check_script + + if options.use_tls: + if options.tls_module is None: + if _import_ssl(): + options.tls_module = _TLS_BY_STANDARD_MODULE + logging.debug('Using ssl module') + elif _import_pyopenssl(): + options.tls_module = _TLS_BY_PYOPENSSL + logging.debug('Using pyOpenSSL module') + else: + logging.critical( + 'TLS support requires ssl or pyOpenSSL module.') + sys.exit(1) + elif options.tls_module == _TLS_BY_STANDARD_MODULE: + if not _import_ssl(): + logging.critical('ssl module is not available') + sys.exit(1) + elif options.tls_module == _TLS_BY_PYOPENSSL: + if not _import_pyopenssl(): + logging.critical('pyOpenSSL module is not available') + sys.exit(1) + else: + logging.critical('Invalid --tls-module option: %r', + options.tls_module) + sys.exit(1) + + if not options.private_key or not options.certificate: + logging.critical( + 'To use TLS, specify private_key and certificate.') + sys.exit(1) + + if (options.tls_client_cert_optional and + not options.tls_client_auth): + logging.critical('Client authentication must be enabled to ' + 'specify tls_client_cert_optional') + sys.exit(1) + else: + if options.tls_module is not None: + logging.critical('Use --tls-module option only together with ' + '--use-tls option.') + sys.exit(1) + + if options.tls_client_auth: + logging.critical('TLS must be enabled for client authentication.') + sys.exit(1) + + if options.tls_client_cert_optional: + logging.critical('TLS must be enabled for client authentication.') + sys.exit(1) + + if not options.scan_dir: + options.scan_dir = options.websock_handlers + + if options.use_basic_auth: + options.basic_auth_credential = 'Basic ' + base64.b64encode( + options.basic_auth_credential) + + try: + if options.thread_monitor_interval_in_sec > 0: + # Run a thread monitor to show the status of server threads for + # debugging. + ThreadMonitor(options.thread_monitor_interval_in_sec).start() + + server = WebSocketServer(options) + server.serve_forever() + except Exception, e: + logging.critical('mod_pywebsocket: %s' % e) + logging.critical('mod_pywebsocket: %s' % util.get_stack_trace()) + sys.exit(1) + + +if __name__ == '__main__': + _main(sys.argv[1:]) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/stream.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/stream.py new file mode 100644 index 000000000..edc533279 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/stream.py @@ -0,0 +1,57 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""This file exports public symbols. +""" + + +from mod_pywebsocket._stream_base import BadOperationException +from mod_pywebsocket._stream_base import ConnectionTerminatedException +from mod_pywebsocket._stream_base import InvalidFrameException +from mod_pywebsocket._stream_base import InvalidUTF8Exception +from mod_pywebsocket._stream_base import UnsupportedFrameException +from mod_pywebsocket._stream_hixie75 import StreamHixie75 +from mod_pywebsocket._stream_hybi import Frame +from mod_pywebsocket._stream_hybi import Stream +from mod_pywebsocket._stream_hybi import StreamOptions + +# These methods are intended to be used by WebSocket client developers to have +# their implementations receive broken data in tests. +from mod_pywebsocket._stream_hybi import create_close_frame +from mod_pywebsocket._stream_hybi import create_header +from mod_pywebsocket._stream_hybi import create_length_header +from mod_pywebsocket._stream_hybi import create_ping_frame +from mod_pywebsocket._stream_hybi import create_pong_frame +from mod_pywebsocket._stream_hybi import create_binary_frame +from mod_pywebsocket._stream_hybi import create_text_frame +from mod_pywebsocket._stream_hybi import create_closing_handshake_body + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/util.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/util.py new file mode 100644 index 000000000..d224ae394 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/util.py @@ -0,0 +1,416 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""WebSocket utilities. +""" + + +import array +import errno + +# Import hash classes from a module available and recommended for each Python +# version and re-export those symbol. Use sha and md5 module in Python 2.4, and +# hashlib module in Python 2.6. +try: + import hashlib + md5_hash = hashlib.md5 + sha1_hash = hashlib.sha1 +except ImportError: + import md5 + import sha + md5_hash = md5.md5 + sha1_hash = sha.sha + +import StringIO +import logging +import os +import re +import socket +import traceback +import zlib + +try: + from mod_pywebsocket import fast_masking +except ImportError: + pass + + +def get_stack_trace(): + """Get the current stack trace as string. + + This is needed to support Python 2.3. + TODO: Remove this when we only support Python 2.4 and above. + Use traceback.format_exc instead. + """ + + out = StringIO.StringIO() + traceback.print_exc(file=out) + return out.getvalue() + + +def prepend_message_to_exception(message, exc): + """Prepend message to the exception.""" + + exc.args = (message + str(exc),) + return + + +def __translate_interp(interp, cygwin_path): + """Translate interp program path for Win32 python to run cygwin program + (e.g. perl). Note that it doesn't support path that contains space, + which is typically true for Unix, where #!-script is written. + For Win32 python, cygwin_path is a directory of cygwin binaries. + + Args: + interp: interp command line + cygwin_path: directory name of cygwin binary, or None + Returns: + translated interp command line. + """ + if not cygwin_path: + return interp + m = re.match('^[^ ]*/([^ ]+)( .*)?', interp) + if m: + cmd = os.path.join(cygwin_path, m.group(1)) + return cmd + m.group(2) + return interp + + +def get_script_interp(script_path, cygwin_path=None): + """Gets #!-interpreter command line from the script. + + It also fixes command path. When Cygwin Python is used, e.g. in WebKit, + it could run "/usr/bin/perl -wT hello.pl". + When Win32 Python is used, e.g. in Chromium, it couldn't. So, fix + "/usr/bin/perl" to "<cygwin_path>\perl.exe". + + Args: + script_path: pathname of the script + cygwin_path: directory name of cygwin binary, or None + Returns: + #!-interpreter command line, or None if it is not #!-script. + """ + fp = open(script_path) + line = fp.readline() + fp.close() + m = re.match('^#!(.*)', line) + if m: + return __translate_interp(m.group(1), cygwin_path) + return None + + +def wrap_popen3_for_win(cygwin_path): + """Wrap popen3 to support #!-script on Windows. + + Args: + cygwin_path: path for cygwin binary if command path is needed to be + translated. None if no translation required. + """ + + __orig_popen3 = os.popen3 + + def __wrap_popen3(cmd, mode='t', bufsize=-1): + cmdline = cmd.split(' ') + interp = get_script_interp(cmdline[0], cygwin_path) + if interp: + cmd = interp + ' ' + cmd + return __orig_popen3(cmd, mode, bufsize) + + os.popen3 = __wrap_popen3 + + +def hexify(s): + return ' '.join(map(lambda x: '%02x' % ord(x), s)) + + +def get_class_logger(o): + return logging.getLogger( + '%s.%s' % (o.__class__.__module__, o.__class__.__name__)) + + +class NoopMasker(object): + """A masking object that has the same interface as RepeatedXorMasker but + just returns the string passed in without making any change. + """ + + def __init__(self): + pass + + def mask(self, s): + return s + + +class RepeatedXorMasker(object): + """A masking object that applies XOR on the string given to mask method + with the masking bytes given to the constructor repeatedly. This object + remembers the position in the masking bytes the last mask method call + ended and resumes from that point on the next mask method call. + """ + + def __init__(self, masking_key): + self._masking_key = masking_key + self._masking_key_index = 0 + + def _mask_using_swig(self, s): + masked_data = fast_masking.mask( + s, self._masking_key, self._masking_key_index) + self._masking_key_index = ( + (self._masking_key_index + len(s)) % len(self._masking_key)) + return masked_data + + def _mask_using_array(self, s): + result = array.array('B') + result.fromstring(s) + + # Use temporary local variables to eliminate the cost to access + # attributes + masking_key = map(ord, self._masking_key) + masking_key_size = len(masking_key) + masking_key_index = self._masking_key_index + + for i in xrange(len(result)): + result[i] ^= masking_key[masking_key_index] + masking_key_index = (masking_key_index + 1) % masking_key_size + + self._masking_key_index = masking_key_index + + return result.tostring() + + if 'fast_masking' in globals(): + mask = _mask_using_swig + else: + mask = _mask_using_array + + +# By making wbits option negative, we can suppress CMF/FLG (2 octet) and +# ADLER32 (4 octet) fields of zlib so that we can use zlib module just as +# deflate library. DICTID won't be added as far as we don't set dictionary. +# LZ77 window of 32K will be used for both compression and decompression. +# For decompression, we can just use 32K to cover any windows size. For +# compression, we use 32K so receivers must use 32K. +# +# Compression level is Z_DEFAULT_COMPRESSION. We don't have to match level +# to decode. +# +# See zconf.h, deflate.cc, inflate.cc of zlib library, and zlibmodule.c of +# Python. See also RFC1950 (ZLIB 3.3). + + +class _Deflater(object): + + def __init__(self, window_bits): + self._logger = get_class_logger(self) + + self._compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -window_bits) + + def compress(self, bytes): + compressed_bytes = self._compress.compress(bytes) + self._logger.debug('Compress input %r', bytes) + self._logger.debug('Compress result %r', compressed_bytes) + return compressed_bytes + + def compress_and_flush(self, bytes): + compressed_bytes = self._compress.compress(bytes) + compressed_bytes += self._compress.flush(zlib.Z_SYNC_FLUSH) + self._logger.debug('Compress input %r', bytes) + self._logger.debug('Compress result %r', compressed_bytes) + return compressed_bytes + + def compress_and_finish(self, bytes): + compressed_bytes = self._compress.compress(bytes) + compressed_bytes += self._compress.flush(zlib.Z_FINISH) + self._logger.debug('Compress input %r', bytes) + self._logger.debug('Compress result %r', compressed_bytes) + return compressed_bytes + + +class _Inflater(object): + + def __init__(self, window_bits): + self._logger = get_class_logger(self) + self._window_bits = window_bits + + self._unconsumed = '' + + self.reset() + + def decompress(self, size): + if not (size == -1 or size > 0): + raise Exception('size must be -1 or positive') + + data = '' + + while True: + if size == -1: + data += self._decompress.decompress(self._unconsumed) + # See Python bug http://bugs.python.org/issue12050 to + # understand why the same code cannot be used for updating + # self._unconsumed for here and else block. + self._unconsumed = '' + else: + data += self._decompress.decompress( + self._unconsumed, size - len(data)) + self._unconsumed = self._decompress.unconsumed_tail + if self._decompress.unused_data: + # Encountered a last block (i.e. a block with BFINAL = 1) and + # found a new stream (unused_data). We cannot use the same + # zlib.Decompress object for the new stream. Create a new + # Decompress object to decompress the new one. + # + # It's fine to ignore unconsumed_tail if unused_data is not + # empty. + self._unconsumed = self._decompress.unused_data + self.reset() + if size >= 0 and len(data) == size: + # data is filled. Don't call decompress again. + break + else: + # Re-invoke Decompress.decompress to try to decompress all + # available bytes before invoking read which blocks until + # any new byte is available. + continue + else: + # Here, since unused_data is empty, even if unconsumed_tail is + # not empty, bytes of requested length are already in data. We + # don't have to "continue" here. + break + + if data: + self._logger.debug('Decompressed %r', data) + return data + + def append(self, data): + self._logger.debug('Appended %r', data) + self._unconsumed += data + + def reset(self): + self._logger.debug('Reset') + self._decompress = zlib.decompressobj(-self._window_bits) + + +# Compresses/decompresses given octets using the method introduced in RFC1979. + + +class _RFC1979Deflater(object): + """A compressor class that applies DEFLATE to given byte sequence and + flushes using the algorithm described in the RFC1979 section 2.1. + """ + + def __init__(self, window_bits, no_context_takeover): + self._deflater = None + if window_bits is None: + window_bits = zlib.MAX_WBITS + self._window_bits = window_bits + self._no_context_takeover = no_context_takeover + + def filter(self, bytes, end=True, bfinal=False): + if self._deflater is None: + self._deflater = _Deflater(self._window_bits) + + if bfinal: + result = self._deflater.compress_and_finish(bytes) + # Add a padding block with BFINAL = 0 and BTYPE = 0. + result = result + chr(0) + self._deflater = None + return result + + result = self._deflater.compress_and_flush(bytes) + if end: + # Strip last 4 octets which is LEN and NLEN field of a + # non-compressed block added for Z_SYNC_FLUSH. + result = result[:-4] + + if self._no_context_takeover and end: + self._deflater = None + + return result + + +class _RFC1979Inflater(object): + """A decompressor class for byte sequence compressed and flushed following + the algorithm described in the RFC1979 section 2.1. + """ + + def __init__(self, window_bits=zlib.MAX_WBITS): + self._inflater = _Inflater(window_bits) + + def filter(self, bytes): + # Restore stripped LEN and NLEN field of a non-compressed block added + # for Z_SYNC_FLUSH. + self._inflater.append(bytes + '\x00\x00\xff\xff') + return self._inflater.decompress(-1) + + +class DeflateSocket(object): + """A wrapper class for socket object to intercept send and recv to perform + deflate compression and decompression transparently. + """ + + # Size of the buffer passed to recv to receive compressed data. + _RECV_SIZE = 4096 + + def __init__(self, socket): + self._socket = socket + + self._logger = get_class_logger(self) + + self._deflater = _Deflater(zlib.MAX_WBITS) + self._inflater = _Inflater(zlib.MAX_WBITS) + + def recv(self, size): + """Receives data from the socket specified on the construction up + to the specified size. Once any data is available, returns it even + if it's smaller than the specified size. + """ + + # TODO(tyoshino): Allow call with size=0. It should block until any + # decompressed data is available. + if size <= 0: + raise Exception('Non-positive size passed') + while True: + data = self._inflater.decompress(size) + if len(data) != 0: + return data + + read_data = self._socket.recv(DeflateSocket._RECV_SIZE) + if not read_data: + return '' + self._inflater.append(read_data) + + def sendall(self, bytes): + self.send(bytes) + + def send(self, bytes): + self._socket.sendall(self._deflater.compress_and_flush(bytes)) + return len(bytes) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/xhr_benchmark_handler.py b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/xhr_benchmark_handler.py new file mode 100644 index 000000000..6735c7e2a --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/mod_pywebsocket/xhr_benchmark_handler.py @@ -0,0 +1,109 @@ +# Copyright 2014 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the COPYING file or at +# https://developers.google.com/open-source/licenses/bsd + + +from mod_pywebsocket import util + + +class XHRBenchmarkHandler(object): + def __init__(self, headers, rfile, wfile): + self._logger = util.get_class_logger(self) + + self.headers = headers + self.rfile = rfile + self.wfile = wfile + + def do_send(self): + content_length = int(self.headers.getheader('Content-Length')) + + self._logger.debug('Requested to receive %s bytes', content_length) + + RECEIVE_BLOCK_SIZE = 1024 * 1024 + + bytes_to_receive = content_length + while bytes_to_receive > 0: + bytes_to_receive_in_this_loop = bytes_to_receive + if bytes_to_receive_in_this_loop > RECEIVE_BLOCK_SIZE: + bytes_to_receive_in_this_loop = RECEIVE_BLOCK_SIZE + received_data = self.rfile.read(bytes_to_receive_in_this_loop) + if received_data != ('a' * bytes_to_receive_in_this_loop): + self._logger.debug('Request body verification failed') + return + bytes_to_receive -= len(received_data) + if bytes_to_receive < 0: + self._logger.debug('Received %d more bytes than expected' % + (-bytes_to_receive)) + return + + # Return the number of received bytes back to the client. + response_body = '%d' % content_length + self.wfile.write( + 'HTTP/1.1 200 OK\r\n' + 'Content-Type: text/html\r\n' + 'Content-Length: %d\r\n' + '\r\n%s' % (len(response_body), response_body)) + self.wfile.flush() + + def do_receive(self): + content_length = int(self.headers.getheader('Content-Length')) + request_body = self.rfile.read(content_length) + + request_array = request_body.split(' ') + if len(request_array) < 2: + self._logger.debug('Malformed request body: %r', request_body) + return + + # Parse the size parameter. + bytes_to_send = request_array[0] + try: + bytes_to_send = int(bytes_to_send) + except ValueError, e: + self._logger.debug('Malformed size parameter: %r', bytes_to_send) + return + self._logger.debug('Requested to send %s bytes', bytes_to_send) + + # Parse the transfer encoding parameter. + chunked_mode = False + mode_parameter = request_array[1] + if mode_parameter == 'chunked': + self._logger.debug('Requested chunked transfer encoding') + chunked_mode = True + elif mode_parameter != 'none': + self._logger.debug('Invalid mode parameter: %r', mode_parameter) + return + + # Write a header + response_header = ( + 'HTTP/1.1 200 OK\r\n' + 'Content-Type: application/octet-stream\r\n') + if chunked_mode: + response_header += 'Transfer-Encoding: chunked\r\n\r\n' + else: + response_header += ( + 'Content-Length: %d\r\n\r\n' % bytes_to_send) + self.wfile.write(response_header) + self.wfile.flush() + + # Write a body + SEND_BLOCK_SIZE = 1024 * 1024 + + while bytes_to_send > 0: + bytes_to_send_in_this_loop = bytes_to_send + if bytes_to_send_in_this_loop > SEND_BLOCK_SIZE: + bytes_to_send_in_this_loop = SEND_BLOCK_SIZE + + if chunked_mode: + self.wfile.write('%x\r\n' % bytes_to_send_in_this_loop) + self.wfile.write('a' * bytes_to_send_in_this_loop) + if chunked_mode: + self.wfile.write('\r\n') + self.wfile.flush() + + bytes_to_send -= bytes_to_send_in_this_loop + + if chunked_mode: + self.wfile.write('0\r\n\r\n') + self.wfile.flush() diff --git a/testing/web-platform/tests/tools/pywebsocket/src/setup.py b/testing/web-platform/tests/tools/pywebsocket/src/setup.py new file mode 100755 index 000000000..ada8db3e1 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/setup.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Set up script for mod_pywebsocket. +""" + + +from distutils.core import setup, Extension +import sys + + +_PACKAGE_NAME = 'mod_pywebsocket' + +# Build and use a C++ extension for faster masking. SWIG is required. +_USE_FAST_MASKING = False + +if sys.version < '2.3': + print >> sys.stderr, '%s requires Python 2.3 or later.' % _PACKAGE_NAME + sys.exit(1) + +if _USE_FAST_MASKING: + setup(ext_modules=[ + Extension( + 'mod_pywebsocket/_fast_masking', + ['mod_pywebsocket/fast_masking.i'], + swig_opts=['-c++'])]) + +setup(author='Yuzo Fujishima', + author_email='yuzo@chromium.org', + description='WebSocket extension for Apache HTTP Server.', + long_description=( + 'mod_pywebsocket is an Apache HTTP Server extension for ' + 'the WebSocket Protocol (RFC 6455). ' + 'See mod_pywebsocket/__init__.py for more detail.'), + license='See COPYING', + name=_PACKAGE_NAME, + packages=[_PACKAGE_NAME, _PACKAGE_NAME + '.handshake'], + url='http://code.google.com/p/pywebsocket/', + # See the source of distutils.version, distutils.versionpredicate and + # distutils.dist to understand how to name version numbers. + version='0.7.9', + ) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/__init__.py b/testing/web-platform/tests/tools/pywebsocket/src/test/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/__init__.py diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cacert.pem b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cacert.pem new file mode 100644 index 000000000..4dadae121 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cacert.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICvDCCAiWgAwIBAgIJAKqVghkGF1rSMA0GCSqGSIb3DQEBBQUAMEkxCzAJBgNV +BAYTAkpQMQ4wDAYDVQQIEwVUb2t5bzEUMBIGA1UEChMLcHl3ZWJzb2NrZXQxFDAS +BgNVBAMTC3B5d2Vic29ja2V0MB4XDTEyMDYwNjA3MjQzM1oXDTM5MTAyMzA3MjQz +M1owSTELMAkGA1UEBhMCSlAxDjAMBgNVBAgTBVRva3lvMRQwEgYDVQQKEwtweXdl +YnNvY2tldDEUMBIGA1UEAxMLcHl3ZWJzb2NrZXQwgZ8wDQYJKoZIhvcNAQEBBQAD +gY0AMIGJAoGBAKoSEW2biQxVrMMKdn/8PJzDYiSXDPR9WQbLRRQ1Gm5jkCYiahXW +u2CbTThfPPfi2NHA3I+HlT7gO9yR7RVUvN6ISUzGwXDEq4f4UNqtQOhQaqqK+CZ9 +LO/BhO/YYfNrbSPlYzHUKaT9ese7xO9VzVKLW+qUf2Mjh4/+SzxBDNP7AgMBAAGj +gaswgagwHQYDVR0OBBYEFOsWdxCSuyhwaZeab6BoTho3++bzMHkGA1UdIwRyMHCA +FOsWdxCSuyhwaZeab6BoTho3++bzoU2kSzBJMQswCQYDVQQGEwJKUDEOMAwGA1UE +CBMFVG9reW8xFDASBgNVBAoTC3B5d2Vic29ja2V0MRQwEgYDVQQDEwtweXdlYnNv +Y2tldIIJAKqVghkGF1rSMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEA +gsMI1WEYqNw/jhUIdrTBcCxJ0X6hJvA9ziKANVm1Rs+4P3YDArkQ8bCr6xY+Kw7s +Zp0yE7dM8GMdi+DU6hL3t3E5eMkTS1yZr9WCK4f2RLo+et98selZydpHemF3DJJ3 +gAj8Sx4LBaG8Cb/WnEMPv3MxG3fBE5favF6V4jU07hQ= +-----END CERTIFICATE----- diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cert.pem b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cert.pem new file mode 100644 index 000000000..25379a72b --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cert.pem @@ -0,0 +1,61 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=JP, ST=Tokyo, O=pywebsocket, CN=pywebsocket + Validity + Not Before: Jun 6 07:25:08 2012 GMT + Not After : Oct 23 07:25:08 2039 GMT + Subject: C=JP, ST=Tokyo, O=pywebsocket, CN=pywebsocket + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:de:10:ce:3a:5a:04:a4:1c:29:93:5c:23:82:1a: + f2:06:01:e6:2b:a4:0f:dd:77:49:76:89:03:a2:21: + de:04:75:c6:e2:dd:fb:35:27:3a:a2:92:8e:12:62: + 2b:3e:1f:f4:78:df:b6:94:cb:27:d6:cb:d6:37:d7: + 5c:08:f0:09:3e:c9:ce:24:2d:00:c9:df:4a:e0:99: + e5:fb:23:a9:e2:d6:c9:3d:96:fa:01:88:de:5a:89: + b0:cf:03:67:6f:04:86:1d:ef:62:1c:55:a9:07:9a: + 2e:66:2a:73:5b:4c:62:03:f9:82:83:db:68:bf:b8: + 4b:0b:8b:93:11:b8:54:73:7b + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Cert Type: + SSL Server + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 82:A1:73:8B:16:0C:7C:E4:D3:46:95:13:95:1A:32:C1:84:E9:06:00 + X509v3 Authority Key Identifier: + keyid:EB:16:77:10:92:BB:28:70:69:97:9A:6F:A0:68:4E:1A:37:FB:E6:F3 + + Signature Algorithm: sha1WithRSAEncryption + 6b:b3:46:29:02:df:b0:c8:8e:c4:d7:7f:a0:1e:0d:1a:eb:2f: + df:d1:48:57:36:5f:95:8c:1b:f0:51:d6:52:e7:8d:84:3b:9f: + d8:ed:22:9c:aa:bd:ee:9b:90:1d:84:a3:4c:0b:cb:eb:64:73: + ba:f7:15:ce:da:5f:db:8b:15:07:a6:28:7f:b9:8c:11:9b:64: + d3:f1:be:52:4f:c3:d8:58:fe:de:56:63:63:3b:51:ed:a7:81: + f9:05:51:70:63:32:09:0e:94:7e:05:fe:a1:56:18:34:98:d5: + 99:1e:4e:27:38:89:90:6a:e5:ce:60:35:01:f5:de:34:60:b1: + cb:ae +-----BEGIN CERTIFICATE----- +MIICmDCCAgGgAwIBAgIBATANBgkqhkiG9w0BAQUFADBJMQswCQYDVQQGEwJKUDEO +MAwGA1UECBMFVG9reW8xFDASBgNVBAoTC3B5d2Vic29ja2V0MRQwEgYDVQQDEwtw +eXdlYnNvY2tldDAeFw0xMjA2MDYwNzI1MDhaFw0zOTEwMjMwNzI1MDhaMEkxCzAJ +BgNVBAYTAkpQMQ4wDAYDVQQIEwVUb2t5bzEUMBIGA1UEChMLcHl3ZWJzb2NrZXQx +FDASBgNVBAMTC3B5d2Vic29ja2V0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQDeEM46WgSkHCmTXCOCGvIGAeYrpA/dd0l2iQOiId4Edcbi3fs1Jzqiko4SYis+ +H/R437aUyyfWy9Y311wI8Ak+yc4kLQDJ30rgmeX7I6ni1sk9lvoBiN5aibDPA2dv +BIYd72IcVakHmi5mKnNbTGID+YKD22i/uEsLi5MRuFRzewIDAQABo4GPMIGMMAkG +A1UdEwQCMAAwEQYJYIZIAYb4QgEBBAQDAgZAMCwGCWCGSAGG+EIBDQQfFh1PcGVu +U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUgqFzixYMfOTTRpUT +lRoywYTpBgAwHwYDVR0jBBgwFoAU6xZ3EJK7KHBpl5pvoGhOGjf75vMwDQYJKoZI +hvcNAQEFBQADgYEAa7NGKQLfsMiOxNd/oB4NGusv39FIVzZflYwb8FHWUueNhDuf +2O0inKq97puQHYSjTAvL62RzuvcVztpf24sVB6Yof7mMEZtk0/G+Uk/D2Fj+3lZj +YztR7aeB+QVRcGMyCQ6UfgX+oVYYNJjVmR5OJziJkGrlzmA1AfXeNGCxy64= +-----END CERTIFICATE----- diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/cert/client_cert.p12 b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/client_cert.p12 Binary files differnew file mode 100644 index 000000000..14e139927 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/client_cert.p12 diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/cert/key.pem b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/key.pem new file mode 100644 index 000000000..fae858318 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDeEM46WgSkHCmTXCOCGvIGAeYrpA/dd0l2iQOiId4Edcbi3fs1 +Jzqiko4SYis+H/R437aUyyfWy9Y311wI8Ak+yc4kLQDJ30rgmeX7I6ni1sk9lvoB +iN5aibDPA2dvBIYd72IcVakHmi5mKnNbTGID+YKD22i/uEsLi5MRuFRzewIDAQAB +AoGBAIuCuV1Vcnb7rm8CwtgZP5XgmY8vSjxTldafa6XvawEYUTP0S77v/1llg1Yv +UIV+I+PQgG9oVoYOl22LoimHS/Z3e1fsot5tDYszGe8/Gkst4oaReSoxvBUa6WXp +QSo7YFCajuHtE+W/gzF+UHbdzzXIDjQZ314LNF5t+4UnsEPBAkEA+girImqWoM2t +3UR8f8oekERwsmEMf9DH5YpH4cvUnvI+kwesC/r2U8Sho++fyEMUNm7aIXGqNLga +ogAM+4NX4QJBAONdSxSay22egTGNoIhLndljWkuOt/9FWj2klf/4QxD4blMJQ5Oq +QdOGAh7nVQjpPLQ5D7CBVAKpGM2CD+QJBtsCQEP2kz35pxPylG3urcC2mfQxBkkW +ZCViBNP58GwJ0bOauTOSBEwFXWuLqTw8aDwxL49UNmqc0N0fpe2fAehj3UECQQCm +FH/DjU8Lw7ybddjNtm6XXPuYNagxz3cbkB4B3FchDleIUDwMoVF0MW9bI5/54mV1 +QDk1tUKortxvQZJaAD4BAkEAhGOHQqPd6bBBoFBvpaLzPJMxwLKrB+Wtkq/QlC72 +ClRiMn2g8SALiIL3BDgGXKcKE/Wy7jo/af/JCzQ/cPqt/A== +-----END RSA PRIVATE KEY----- diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/client_for_testing.py b/testing/web-platform/tests/tools/pywebsocket/src/test/client_for_testing.py new file mode 100644 index 000000000..c7f805ee9 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/client_for_testing.py @@ -0,0 +1,1100 @@ +#!/usr/bin/env python +# +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""WebSocket client utility for testing. + +This module contains helper methods for performing handshake, frame +sending/receiving as a WebSocket client. + +This is code for testing mod_pywebsocket. Keep this code independent from +mod_pywebsocket. Don't import e.g. Stream class for generating frame for +testing. Using util.hexify, etc. that are not related to protocol processing +is allowed. + +Note: +This code is far from robust, e.g., we cut corners in handshake. +""" + + +import base64 +import errno +import logging +import os +import random +import re +import socket +import struct +import time + +from mod_pywebsocket import common +from mod_pywebsocket import util + + +DEFAULT_PORT = 80 +DEFAULT_SECURE_PORT = 443 + +# Opcodes introduced in IETF HyBi 01 for the new framing format +OPCODE_CONTINUATION = 0x0 +OPCODE_CLOSE = 0x8 +OPCODE_PING = 0x9 +OPCODE_PONG = 0xa +OPCODE_TEXT = 0x1 +OPCODE_BINARY = 0x2 + +# Strings used for handshake +_UPGRADE_HEADER = 'Upgrade: websocket\r\n' +_UPGRADE_HEADER_HIXIE75 = 'Upgrade: WebSocket\r\n' +_CONNECTION_HEADER = 'Connection: Upgrade\r\n' + +WEBSOCKET_ACCEPT_UUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + +# Status codes +STATUS_NORMAL_CLOSURE = 1000 +STATUS_GOING_AWAY = 1001 +STATUS_PROTOCOL_ERROR = 1002 +STATUS_UNSUPPORTED_DATA = 1003 +STATUS_NO_STATUS_RECEIVED = 1005 +STATUS_ABNORMAL_CLOSURE = 1006 +STATUS_INVALID_FRAME_PAYLOAD_DATA = 1007 +STATUS_POLICY_VIOLATION = 1008 +STATUS_MESSAGE_TOO_BIG = 1009 +STATUS_MANDATORY_EXT = 1010 +STATUS_INTERNAL_ENDPOINT_ERROR = 1011 +STATUS_TLS_HANDSHAKE = 1015 + +# Extension tokens +_DEFLATE_FRAME_EXTENSION = 'deflate-frame' +# TODO(bashi): Update after mux implementation finished. +_MUX_EXTENSION = 'mux_DO_NOT_USE' +_PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate' + +def _method_line(resource): + return 'GET %s HTTP/1.1\r\n' % resource + + +def _sec_origin_header(origin): + return 'Sec-WebSocket-Origin: %s\r\n' % origin.lower() + + +def _origin_header(origin): + # 4.1 13. concatenation of the string "Origin:", a U+0020 SPACE character, + # and the /origin/ value, converted to ASCII lowercase, to /fields/. + return 'Origin: %s\r\n' % origin.lower() + + +def _format_host_header(host, port, secure): + # 4.1 9. Let /hostport/ be an empty string. + # 4.1 10. Append the /host/ value, converted to ASCII lowercase, to + # /hostport/ + hostport = host.lower() + # 4.1 11. If /secure/ is false, and /port/ is not 80, or if /secure/ + # is true, and /port/ is not 443, then append a U+003A COLON character + # (:) followed by the value of /port/, expressed as a base-ten integer, + # to /hostport/ + if ((not secure and port != DEFAULT_PORT) or + (secure and port != DEFAULT_SECURE_PORT)): + hostport += ':' + str(port) + # 4.1 12. concatenation of the string "Host:", a U+0020 SPACE + # character, and /hostport/, to /fields/. + return 'Host: %s\r\n' % hostport + + +# TODO(tyoshino): Define a base class and move these shared methods to that. + + +def receive_bytes(socket, length): + received_bytes = [] + remaining = length + while remaining > 0: + new_received_bytes = socket.recv(remaining) + if not new_received_bytes: + raise Exception( + 'Connection closed before receiving requested length ' + '(requested %d bytes but received only %d bytes)' % + (length, length - remaining)) + received_bytes.append(new_received_bytes) + remaining -= len(new_received_bytes) + return ''.join(received_bytes) + + +# TODO(tyoshino): Now the WebSocketHandshake class diverts these methods. We +# should move to HTTP parser as specified in RFC 6455. For HyBi 00 and +# Hixie 75, pack these methods as some parser class. + + +def _read_fields(socket): + # 4.1 32. let /fields/ be a list of name-value pairs, initially empty. + fields = {} + while True: + # 4.1 33. let /name/ and /value/ be empty byte arrays + name = '' + value = '' + # 4.1 34. read /name/ + name = _read_name(socket) + if name is None: + break + # 4.1 35. read spaces + # TODO(tyoshino): Skip only one space as described in the spec. + ch = _skip_spaces(socket) + # 4.1 36. read /value/ + value = _read_value(socket, ch) + # 4.1 37. read a byte from the server + ch = receive_bytes(socket, 1) + if ch != '\n': # 0x0A + raise Exception( + 'Expected LF but found %r while reading value %r for header ' + '%r' % (ch, name, value)) + # 4.1 38. append an entry to the /fields/ list that has the name + # given by the string obtained by interpreting the /name/ byte + # array as a UTF-8 stream and the value given by the string + # obtained by interpreting the /value/ byte array as a UTF-8 byte + # stream. + fields.setdefault(name, []).append(value) + # 4.1 39. return to the "Field" step above + return fields + + +def _read_name(socket): + # 4.1 33. let /name/ be empty byte arrays + name = '' + while True: + # 4.1 34. read a byte from the server + ch = receive_bytes(socket, 1) + if ch == '\r': # 0x0D + return None + elif ch == '\n': # 0x0A + raise Exception( + 'Unexpected LF when reading header name %r' % name) + elif ch == ':': # 0x3A + return name + elif ch >= 'A' and ch <= 'Z': # range 0x31 to 0x5A + ch = chr(ord(ch) + 0x20) + name += ch + else: + name += ch + + +def _skip_spaces(socket): + # 4.1 35. read a byte from the server + while True: + ch = receive_bytes(socket, 1) + if ch == ' ': # 0x20 + continue + return ch + + +def _read_value(socket, ch): + # 4.1 33. let /value/ be empty byte arrays + value = '' + # 4.1 36. read a byte from server. + while True: + if ch == '\r': # 0x0D + return value + elif ch == '\n': # 0x0A + raise Exception( + 'Unexpected LF when reading header value %r' % value) + else: + value += ch + ch = receive_bytes(socket, 1) + + +def read_frame_header(socket): + received = receive_bytes(socket, 2) + + first_byte = ord(received[0]) + fin = (first_byte >> 7) & 1 + rsv1 = (first_byte >> 6) & 1 + rsv2 = (first_byte >> 5) & 1 + rsv3 = (first_byte >> 4) & 1 + opcode = first_byte & 0xf + + second_byte = ord(received[1]) + mask = (second_byte >> 7) & 1 + payload_length = second_byte & 0x7f + + if mask != 0: + raise Exception( + 'Mask bit must be 0 for frames coming from server') + + if payload_length == 127: + extended_payload_length = receive_bytes(socket, 8) + payload_length = struct.unpack( + '!Q', extended_payload_length)[0] + if payload_length > 0x7FFFFFFFFFFFFFFF: + raise Exception('Extended payload length >= 2^63') + elif payload_length == 126: + extended_payload_length = receive_bytes(socket, 2) + payload_length = struct.unpack( + '!H', extended_payload_length)[0] + + return fin, rsv1, rsv2, rsv3, opcode, payload_length + + +class _TLSSocket(object): + """Wrapper for a TLS connection.""" + + def __init__(self, raw_socket): + self._ssl = socket.ssl(raw_socket) + + def send(self, bytes): + return self._ssl.write(bytes) + + def recv(self, size=-1): + return self._ssl.read(size) + + def close(self): + # Nothing to do. + pass + + +class HttpStatusException(Exception): + """This exception will be raised when unexpected http status code was + received as a result of handshake. + """ + + def __init__(self, name, status): + super(HttpStatusException, self).__init__(name) + self.status = status + + +class WebSocketHandshake(object): + """Opening handshake processor for the WebSocket protocol (RFC 6455).""" + + def __init__(self, options): + self._logger = util.get_class_logger(self) + + self._options = options + + def handshake(self, socket): + """Handshake WebSocket. + + Raises: + Exception: handshake failed. + """ + + self._socket = socket + + request_line = _method_line(self._options.resource) + self._logger.debug('Opening handshake Request-Line: %r', request_line) + self._socket.sendall(request_line) + + fields = [] + fields.append(_UPGRADE_HEADER) + fields.append(_CONNECTION_HEADER) + + fields.append(_format_host_header( + self._options.server_host, + self._options.server_port, + self._options.use_tls)) + + if self._options.version is 8: + fields.append(_sec_origin_header(self._options.origin)) + else: + fields.append(_origin_header(self._options.origin)) + + original_key = os.urandom(16) + key = base64.b64encode(original_key) + self._logger.debug( + 'Sec-WebSocket-Key: %s (%s)', key, util.hexify(original_key)) + fields.append('Sec-WebSocket-Key: %s\r\n' % key) + + fields.append('Sec-WebSocket-Version: %d\r\n' % self._options.version) + + # Setting up extensions. + if len(self._options.extensions) > 0: + fields.append('Sec-WebSocket-Extensions: %s\r\n' % + ', '.join(self._options.extensions)) + + self._logger.debug('Opening handshake request headers: %r', fields) + + for field in fields: + self._socket.sendall(field) + self._socket.sendall('\r\n') + + self._logger.info('Sent opening handshake request') + + field = '' + while True: + ch = receive_bytes(self._socket, 1) + field += ch + if ch == '\n': + break + + self._logger.debug('Opening handshake Response-Line: %r', field) + + if len(field) < 7 or not field.endswith('\r\n'): + raise Exception('Wrong status line: %r' % field) + m = re.match('[^ ]* ([^ ]*) .*', field) + if m is None: + raise Exception( + 'No HTTP status code found in status line: %r' % field) + code = m.group(1) + if not re.match('[0-9][0-9][0-9]', code): + raise Exception( + 'HTTP status code %r is not three digit in status line: %r' % + (code, field)) + if code != '101': + raise HttpStatusException( + 'Expected HTTP status code 101 but found %r in status line: ' + '%r' % (code, field), int(code)) + fields = _read_fields(self._socket) + ch = receive_bytes(self._socket, 1) + if ch != '\n': # 0x0A + raise Exception('Expected LF but found: %r' % ch) + + self._logger.debug('Opening handshake response headers: %r', fields) + + # Check /fields/ + if len(fields['upgrade']) != 1: + raise Exception( + 'Multiple Upgrade headers found: %s' % fields['upgrade']) + if len(fields['connection']) != 1: + raise Exception( + 'Multiple Connection headers found: %s' % fields['connection']) + if fields['upgrade'][0] != 'websocket': + raise Exception( + 'Unexpected Upgrade header value: %s' % fields['upgrade'][0]) + if fields['connection'][0].lower() != 'upgrade': + raise Exception( + 'Unexpected Connection header value: %s' % + fields['connection'][0]) + + if len(fields['sec-websocket-accept']) != 1: + raise Exception( + 'Multiple Sec-WebSocket-Accept headers found: %s' % + fields['sec-websocket-accept']) + + accept = fields['sec-websocket-accept'][0] + + # Validate + try: + decoded_accept = base64.b64decode(accept) + except TypeError, e: + raise HandshakeException( + 'Illegal value for header Sec-WebSocket-Accept: ' + accept) + + if len(decoded_accept) != 20: + raise HandshakeException( + 'Decoded value of Sec-WebSocket-Accept is not 20-byte long') + + self._logger.debug('Actual Sec-WebSocket-Accept: %r (%s)', + accept, util.hexify(decoded_accept)) + + original_expected_accept = util.sha1_hash( + key + WEBSOCKET_ACCEPT_UUID).digest() + expected_accept = base64.b64encode(original_expected_accept) + + self._logger.debug('Expected Sec-WebSocket-Accept: %r (%s)', + expected_accept, + util.hexify(original_expected_accept)) + + if accept != expected_accept: + raise Exception( + 'Invalid Sec-WebSocket-Accept header: %r (expected) != %r ' + '(actual)' % (accept, expected_accept)) + + server_extensions_header = fields.get('sec-websocket-extensions') + accepted_extensions = [] + if server_extensions_header is not None: + accepted_extensions = common.parse_extensions( + ', '.join(server_extensions_header)) + + # Scan accepted extension list to check if there is any unrecognized + # extensions or extensions we didn't request in it. Then, for + # extensions we request, parse them and store parameters. They will be + # used later by each extension. + deflate_frame_accepted = False + mux_accepted = False + for extension in accepted_extensions: + if extension.name() == _DEFLATE_FRAME_EXTENSION: + if self._options.use_deflate_frame: + deflate_frame_accepted = True + continue + if extension.name() == _MUX_EXTENSION: + if self._options.use_mux: + mux_accepted = True + continue + if extension.name() == _PERMESSAGE_DEFLATE_EXTENSION: + checker = self._options.check_permessage_deflate + if checker: + checker(extension) + continue + + raise Exception( + 'Received unrecognized extension: %s' % extension.name()) + + # Let all extensions check the response for extension request. + + if (self._options.use_deflate_frame and + not deflate_frame_accepted): + raise Exception('%s extension not accepted' % + _DEFLATE_FRAME_EXTENSION) + + if self._options.use_mux and not mux_accepted: + raise Exception('%s extension not accepted' % _MUX_EXTENSION) + + +class WebSocketHybi00Handshake(object): + """Opening handshake processor for the WebSocket protocol version HyBi 00. + """ + + def __init__(self, options, draft_field): + self._logger = util.get_class_logger(self) + + self._options = options + self._draft_field = draft_field + + def handshake(self, socket): + """Handshake WebSocket. + + Raises: + Exception: handshake failed. + """ + + self._socket = socket + + # 4.1 5. send request line. + request_line = _method_line(self._options.resource) + self._logger.debug('Opening handshake Request-Line: %r', request_line) + self._socket.sendall(request_line) + # 4.1 6. Let /fields/ be an empty list of strings. + fields = [] + # 4.1 7. Add the string "Upgrade: WebSocket" to /fields/. + fields.append(_UPGRADE_HEADER_HIXIE75) + # 4.1 8. Add the string "Connection: Upgrade" to /fields/. + fields.append(_CONNECTION_HEADER) + # 4.1 9-12. Add Host: field to /fields/. + fields.append(_format_host_header( + self._options.server_host, + self._options.server_port, + self._options.use_tls)) + # 4.1 13. Add Origin: field to /fields/. + fields.append(_origin_header(self._options.origin)) + # TODO: 4.1 14 Add Sec-WebSocket-Protocol: field to /fields/. + # TODO: 4.1 15 Add cookie headers to /fields/. + + # 4.1 16-23. Add Sec-WebSocket-Key<n> to /fields/. + self._number1, key1 = self._generate_sec_websocket_key() + self._logger.debug('Number1: %d', self._number1) + fields.append('Sec-WebSocket-Key1: %s\r\n' % key1) + self._number2, key2 = self._generate_sec_websocket_key() + self._logger.debug('Number2: %d', self._number1) + fields.append('Sec-WebSocket-Key2: %s\r\n' % key2) + + fields.append('Sec-WebSocket-Draft: %s\r\n' % self._draft_field) + + # 4.1 24. For each string in /fields/, in a random order: send the + # string, encoded as UTF-8, followed by a UTF-8 encoded U+000D CARRIAGE + # RETURN U+000A LINE FEED character pair (CRLF). + random.shuffle(fields) + + self._logger.debug('Opening handshake request headers: %r', fields) + for field in fields: + self._socket.sendall(field) + + # 4.1 25. send a UTF-8-encoded U+000D CARRIAGE RETURN U+000A LINE FEED + # character pair (CRLF). + self._socket.sendall('\r\n') + # 4.1 26. let /key3/ be a string consisting of eight random bytes (or + # equivalently, a random 64 bit integer encoded in a big-endian order). + self._key3 = self._generate_key3() + # 4.1 27. send /key3/ to the server. + self._socket.sendall(self._key3) + self._logger.debug( + 'Key3: %r (%s)', self._key3, util.hexify(self._key3)) + + self._logger.info('Sent opening handshake request') + + # 4.1 28. Read bytes from the server until either the connection + # closes, or a 0x0A byte is read. let /field/ be these bytes, including + # the 0x0A bytes. + field = '' + while True: + ch = receive_bytes(self._socket, 1) + field += ch + if ch == '\n': + break + + self._logger.debug('Opening handshake Response-Line: %r', field) + + # if /field/ is not at least seven bytes long, or if the last + # two bytes aren't 0x0D and 0x0A respectively, or if it does not + # contain at least two 0x20 bytes, then fail the WebSocket connection + # and abort these steps. + if len(field) < 7 or not field.endswith('\r\n'): + raise Exception('Wrong status line: %r' % field) + m = re.match('[^ ]* ([^ ]*) .*', field) + if m is None: + raise Exception('No code found in status line: %r' % field) + # 4.1 29. let /code/ be the substring of /field/ that starts from the + # byte after the first 0x20 byte, and ends with the byte before the + # second 0x20 byte. + code = m.group(1) + # 4.1 30. if /code/ is not three bytes long, or if any of the bytes in + # /code/ are not in the range 0x30 to 0x90, then fail the WebSocket + # connection and abort these steps. + if not re.match('[0-9][0-9][0-9]', code): + raise Exception( + 'HTTP status code %r is not three digit in status line: %r' % + (code, field)) + # 4.1 31. if /code/, interpreted as UTF-8, is "101", then move to the + # next step. + if code != '101': + raise HttpStatusException( + 'Expected HTTP status code 101 but found %r in status line: ' + '%r' % (code, field), int(code)) + # 4.1 32-39. read fields into /fields/ + fields = _read_fields(self._socket) + + self._logger.debug('Opening handshake response headers: %r', fields) + + # 4.1 40. _Fields processing_ + # read a byte from server + ch = receive_bytes(self._socket, 1) + if ch != '\n': # 0x0A + raise Exception('Expected LF but found %r' % ch) + # 4.1 41. check /fields/ + if len(fields['upgrade']) != 1: + raise Exception( + 'Multiple Upgrade headers found: %s' % fields['upgrade']) + if len(fields['connection']) != 1: + raise Exception( + 'Multiple Connection headers found: %s' % fields['connection']) + if len(fields['sec-websocket-origin']) != 1: + raise Exception( + 'Multiple Sec-WebSocket-Origin headers found: %s' % + fields['sec-sebsocket-origin']) + if len(fields['sec-websocket-location']) != 1: + raise Exception( + 'Multiple Sec-WebSocket-Location headers found: %s' % + fields['sec-sebsocket-location']) + # TODO(ukai): protocol + # if the entry's name is "upgrade" + # if the value is not exactly equal to the string "WebSocket", + # then fail the WebSocket connection and abort these steps. + if fields['upgrade'][0] != 'WebSocket': + raise Exception( + 'Unexpected Upgrade header value: %s' % fields['upgrade'][0]) + # if the entry's name is "connection" + # if the value, converted to ASCII lowercase, is not exactly equal + # to the string "upgrade", then fail the WebSocket connection and + # abort these steps. + if fields['connection'][0].lower() != 'upgrade': + raise Exception( + 'Unexpected Connection header value: %s' % + fields['connection'][0]) + # TODO(ukai): check origin, location, cookie, .. + + # 4.1 42. let /challenge/ be the concatenation of /number_1/, + # expressed as a big endian 32 bit integer, /number_2/, expressed + # as big endian 32 bit integer, and the eight bytes of /key_3/ in the + # order they were sent on the wire. + challenge = struct.pack('!I', self._number1) + challenge += struct.pack('!I', self._number2) + challenge += self._key3 + + self._logger.debug( + 'Challenge: %r (%s)', challenge, util.hexify(challenge)) + + # 4.1 43. let /expected/ be the MD5 fingerprint of /challenge/ as a + # big-endian 128 bit string. + expected = util.md5_hash(challenge).digest() + self._logger.debug( + 'Expected challenge response: %r (%s)', + expected, util.hexify(expected)) + + # 4.1 44. read sixteen bytes from the server. + # let /reply/ be those bytes. + reply = receive_bytes(self._socket, 16) + self._logger.debug( + 'Actual challenge response: %r (%s)', reply, util.hexify(reply)) + + # 4.1 45. if /reply/ does not exactly equal /expected/, then fail + # the WebSocket connection and abort these steps. + if expected != reply: + raise Exception( + 'Bad challenge response: %r (expected) != %r (actual)' % + (expected, reply)) + # 4.1 46. The *WebSocket connection is established*. + + def _generate_sec_websocket_key(self): + # 4.1 16. let /spaces_n/ be a random integer from 1 to 12 inclusive. + spaces = random.randint(1, 12) + # 4.1 17. let /max_n/ be the largest integer not greater than + # 4,294,967,295 divided by /spaces_n/. + maxnum = 4294967295 / spaces + # 4.1 18. let /number_n/ be a random integer from 0 to /max_n/ + # inclusive. + number = random.randint(0, maxnum) + # 4.1 19. let /product_n/ be the result of multiplying /number_n/ and + # /spaces_n/ together. + product = number * spaces + # 4.1 20. let /key_n/ be a string consisting of /product_n/, expressed + # in base ten using the numerals in the range U+0030 DIGIT ZERO (0) to + # U+0039 DIGIT NINE (9). + key = str(product) + # 4.1 21. insert between one and twelve random characters from the + # range U+0021 to U+002F and U+003A to U+007E into /key_n/ at random + # positions. + available_chars = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1) + n = random.randint(1, 12) + for _ in xrange(n): + ch = random.choice(available_chars) + pos = random.randint(0, len(key)) + key = key[0:pos] + chr(ch) + key[pos:] + # 4.1 22. insert /spaces_n/ U+0020 SPACE characters into /key_n/ at + # random positions other than start or end of the string. + for _ in xrange(spaces): + pos = random.randint(1, len(key) - 1) + key = key[0:pos] + ' ' + key[pos:] + return number, key + + def _generate_key3(self): + # 4.1 26. let /key3/ be a string consisting of eight random bytes (or + # equivalently, a random 64 bit integer encoded in a big-endian order). + return ''.join([chr(random.randint(0, 255)) for _ in xrange(8)]) + + +class WebSocketHixie75Handshake(object): + """WebSocket handshake processor for IETF Hixie 75.""" + + _EXPECTED_RESPONSE = ( + 'HTTP/1.1 101 Web Socket Protocol Handshake\r\n' + + _UPGRADE_HEADER_HIXIE75 + + _CONNECTION_HEADER) + + def __init__(self, options): + self._logger = util.get_class_logger(self) + + self._options = options + + def _skip_headers(self): + terminator = '\r\n\r\n' + pos = 0 + while pos < len(terminator): + received = receive_bytes(self._socket, 1) + if received == terminator[pos]: + pos += 1 + elif received == terminator[0]: + pos = 1 + else: + pos = 0 + + def handshake(self, socket): + self._socket = socket + + request_line = _method_line(self._options.resource) + self._logger.debug('Opening handshake Request-Line: %r', request_line) + self._socket.sendall(request_line) + + headers = _UPGRADE_HEADER_HIXIE75 + _CONNECTION_HEADER + headers += _format_host_header( + self._options.server_host, + self._options.server_port, + self._options.use_tls) + headers += _origin_header(self._options.origin) + self._logger.debug('Opening handshake request headers: %r', headers) + self._socket.sendall(headers) + + self._socket.sendall('\r\n') + + self._logger.info('Sent opening handshake request') + + for expected_char in WebSocketHixie75Handshake._EXPECTED_RESPONSE: + received = receive_bytes(self._socket, 1) + if expected_char != received: + raise Exception('Handshake failure') + # We cut corners and skip other headers. + self._skip_headers() + + +class WebSocketStream(object): + """Frame processor for the WebSocket protocol (RFC 6455).""" + + def __init__(self, socket, handshake): + self._handshake = handshake + self._socket = socket + + # Filters applied to application data part of data frames. + self._outgoing_frame_filter = None + self._incoming_frame_filter = None + + if self._handshake._options.use_deflate_frame: + self._outgoing_frame_filter = ( + util._RFC1979Deflater(None, False)) + self._incoming_frame_filter = util._RFC1979Inflater() + + self._fragmented = False + + def _mask_hybi(self, s): + # TODO(tyoshino): os.urandom does open/read/close for every call. If + # performance matters, change this to some library call that generates + # cryptographically secure pseudo random number sequence. + masking_nonce = os.urandom(4) + result = [masking_nonce] + count = 0 + for c in s: + result.append(chr(ord(c) ^ ord(masking_nonce[count]))) + count = (count + 1) % len(masking_nonce) + return ''.join(result) + + def send_frame_of_arbitrary_bytes(self, header, body): + self._socket.sendall(header + self._mask_hybi(body)) + + def send_data(self, payload, frame_type, end=True, mask=True, + rsv1=0, rsv2=0, rsv3=0): + if self._outgoing_frame_filter is not None: + payload = self._outgoing_frame_filter.filter(payload) + + if self._fragmented: + opcode = OPCODE_CONTINUATION + else: + opcode = frame_type + + if end: + self._fragmented = False + fin = 1 + else: + self._fragmented = True + fin = 0 + + if self._handshake._options.use_deflate_frame: + rsv1 = 1 + + if mask: + mask_bit = 1 << 7 + else: + mask_bit = 0 + + header = chr(fin << 7 | rsv1 << 6 | rsv2 << 5 | rsv3 << 4 | opcode) + payload_length = len(payload) + if payload_length <= 125: + header += chr(mask_bit | payload_length) + elif payload_length < 1 << 16: + header += chr(mask_bit | 126) + struct.pack('!H', payload_length) + elif payload_length < 1 << 63: + header += chr(mask_bit | 127) + struct.pack('!Q', payload_length) + else: + raise Exception('Too long payload (%d byte)' % payload_length) + if mask: + payload = self._mask_hybi(payload) + self._socket.sendall(header + payload) + + def send_binary(self, payload, end=True, mask=True): + self.send_data(payload, OPCODE_BINARY, end, mask) + + def send_text(self, payload, end=True, mask=True): + self.send_data(payload.encode('utf-8'), OPCODE_TEXT, end, mask) + + def _assert_receive_data(self, payload, opcode, fin, rsv1, rsv2, rsv3): + (actual_fin, actual_rsv1, actual_rsv2, actual_rsv3, actual_opcode, + payload_length) = read_frame_header(self._socket) + + if actual_opcode != opcode: + raise Exception( + 'Unexpected opcode: %d (expected) vs %d (actual)' % + (opcode, actual_opcode)) + + if actual_fin != fin: + raise Exception( + 'Unexpected fin: %d (expected) vs %d (actual)' % + (fin, actual_fin)) + + if rsv1 is None: + rsv1 = 0 + if self._handshake._options.use_deflate_frame: + rsv1 = 1 + + if rsv2 is None: + rsv2 = 0 + + if rsv3 is None: + rsv3 = 0 + + if actual_rsv1 != rsv1: + raise Exception( + 'Unexpected rsv1: %r (expected) vs %r (actual)' % + (rsv1, actual_rsv1)) + + if actual_rsv2 != rsv2: + raise Exception( + 'Unexpected rsv2: %r (expected) vs %r (actual)' % + (rsv2, actual_rsv2)) + + if actual_rsv3 != rsv3: + raise Exception( + 'Unexpected rsv3: %r (expected) vs %r (actual)' % + (rsv3, actual_rsv3)) + + received = receive_bytes(self._socket, payload_length) + + if self._incoming_frame_filter is not None: + received = self._incoming_frame_filter.filter(received) + + if len(received) != len(payload): + raise Exception( + 'Unexpected payload length: %d (expected) vs %d (actual)' % + (len(payload), len(received))) + + if payload != received: + raise Exception( + 'Unexpected payload: %r (expected) vs %r (actual)' % + (payload, received)) + + def assert_receive_binary(self, payload, opcode=OPCODE_BINARY, fin=1, + rsv1=None, rsv2=None, rsv3=None): + self._assert_receive_data(payload, opcode, fin, rsv1, rsv2, rsv3) + + def assert_receive_text(self, payload, opcode=OPCODE_TEXT, fin=1, + rsv1=None, rsv2=None, rsv3=None): + self._assert_receive_data(payload.encode('utf-8'), opcode, fin, rsv1, + rsv2, rsv3) + + def _build_close_frame(self, code, reason, mask): + frame = chr(1 << 7 | OPCODE_CLOSE) + + if code is not None: + body = struct.pack('!H', code) + reason.encode('utf-8') + else: + body = '' + if mask: + frame += chr(1 << 7 | len(body)) + self._mask_hybi(body) + else: + frame += chr(len(body)) + body + return frame + + def send_close(self, code, reason): + self._socket.sendall( + self._build_close_frame(code, reason, True)) + + def assert_receive_close(self, code, reason): + expected_frame = self._build_close_frame(code, reason, False) + actual_frame = receive_bytes(self._socket, len(expected_frame)) + if actual_frame != expected_frame: + raise Exception( + 'Unexpected close frame: %r (expected) vs %r (actual)' % + (expected_frame, actual_frame)) + + +class WebSocketStreamHixie75(object): + """Frame processor for the WebSocket protocol version Hixie 75 and HyBi 00. + """ + + _CLOSE_FRAME = '\xff\x00' + + def __init__(self, socket, unused_handshake): + self._socket = socket + + def send_frame_of_arbitrary_bytes(self, header, body): + self._socket.sendall(header + body) + + def send_data(self, payload, unused_frame_typem, unused_end, unused_mask): + frame = ''.join(['\x00', payload, '\xff']) + self._socket.sendall(frame) + + def send_binary(self, unused_payload, unused_end, unused_mask): + pass + + def send_text(self, payload, unused_end, unused_mask): + encoded_payload = payload.encode('utf-8') + frame = ''.join(['\x00', encoded_payload, '\xff']) + self._socket.sendall(frame) + + def assert_receive_binary(self, payload, opcode=OPCODE_BINARY, fin=1, + rsv1=0, rsv2=0, rsv3=0): + raise Exception('Binary frame is not supported in hixie75') + + def assert_receive_text(self, payload): + received = receive_bytes(self._socket, 1) + + if received != '\x00': + raise Exception( + 'Unexpected frame type: %d (expected) vs %d (actual)' % + (0, ord(received))) + + received = receive_bytes(self._socket, len(payload) + 1) + if received[-1] != '\xff': + raise Exception( + 'Termination expected: 0xff (expected) vs %r (actual)' % + received) + + if received[0:-1] != payload: + raise Exception( + 'Unexpected payload: %r (expected) vs %r (actual)' % + (payload, received[0:-1])) + + def send_close(self, code, reason): + self._socket.sendall(self._CLOSE_FRAME) + + def assert_receive_close(self, unused_code, unused_reason): + closing = receive_bytes(self._socket, len(self._CLOSE_FRAME)) + if closing != self._CLOSE_FRAME: + raise Exception('Didn\'t receive closing handshake') + + +class ClientOptions(object): + """Holds option values to configure the Client object.""" + + def __init__(self): + self.version = 13 + self.server_host = '' + self.origin = '' + self.resource = '' + self.server_port = -1 + self.socket_timeout = 1000 + self.use_tls = False + self.extensions = [] + # Enable deflate-application-data. + self.use_deflate_frame = False + # Enable mux + self.use_mux = False + + def enable_deflate_frame(self): + self.use_deflate_frame = True + self.extensions.append(_DEFLATE_FRAME_EXTENSION) + + def enable_mux(self): + self.use_mux = True + self.extensions.append(_MUX_EXTENSION) + + +def connect_socket_with_retry(host, port, timeout, use_tls, + retry=10, sleep_sec=0.1): + retry_count = 0 + while retry_count < retry: + try: + s = socket.socket() + s.settimeout(timeout) + s.connect((host, port)) + if use_tls: + return _TLSSocket(s) + return s + except socket.error, e: + if e.errno != errno.ECONNREFUSED: + raise + else: + retry_count = retry_count + 1 + time.sleep(sleep_sec) + + return None + + +class Client(object): + """WebSocket client.""" + + def __init__(self, options, handshake, stream_class): + self._logger = util.get_class_logger(self) + + self._options = options + self._socket = None + + self._handshake = handshake + self._stream_class = stream_class + + def connect(self): + self._socket = connect_socket_with_retry( + self._options.server_host, + self._options.server_port, + self._options.socket_timeout, + self._options.use_tls) + + self._handshake.handshake(self._socket) + + self._stream = self._stream_class(self._socket, self._handshake) + + self._logger.info('Connection established') + + def send_frame_of_arbitrary_bytes(self, header, body): + self._stream.send_frame_of_arbitrary_bytes(header, body) + + def send_message(self, message, end=True, binary=False, raw=False, + mask=True): + if binary: + self._stream.send_binary(message, end, mask) + elif raw: + self._stream.send_data(message, OPCODE_TEXT, end, mask) + else: + self._stream.send_text(message, end, mask) + + def assert_receive(self, payload, binary=False): + if binary: + self._stream.assert_receive_binary(payload) + else: + self._stream.assert_receive_text(payload) + + def send_close(self, code=STATUS_NORMAL_CLOSURE, reason=''): + self._stream.send_close(code, reason) + + def assert_receive_close(self, code=STATUS_NORMAL_CLOSURE, reason=''): + self._stream.assert_receive_close(code, reason) + + def close_socket(self): + self._socket.close() + + def assert_connection_closed(self): + try: + read_data = receive_bytes(self._socket, 1) + except Exception, e: + if str(e).find( + 'Connection closed before receiving requested length ') == 0: + return + try: + error_number, message = e + for error_name in ['ECONNRESET', 'WSAECONNRESET']: + if (error_name in dir(errno) and + error_number == getattr(errno, error_name)): + return + except: + raise e + raise e + + raise Exception('Connection is not closed (Read: %r)' % read_data) + + +def create_client(options): + return Client( + options, WebSocketHandshake(options), WebSocketStream) + + +def create_client_hybi00(options): + return Client( + options, + WebSocketHybi00Handshake(options, '0'), + WebSocketStreamHixie75) + + +def create_client_hixie75(options): + return Client( + options, WebSocketHixie75Handshake(options), WebSocketStreamHixie75) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/endtoend_with_external_server.py b/testing/web-platform/tests/tools/pywebsocket/src/test/endtoend_with_external_server.py new file mode 100755 index 000000000..47f86fdb4 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/endtoend_with_external_server.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Test for end-to-end with external server. + +This test is not run by run_all.py because it requires some preparations. +If you would like to run this test correctly, launch Apache with mod_python +and mod_pywebsocket manually. In addition, you should pass allow_draft75 option +and example path as handler_scan option and Apache's DocumentRoot. +""" + + +import optparse +import sys +import test.test_endtoend +import unittest + + +_DEFAULT_WEB_SOCKET_PORT = 80 + + +class EndToEndTestWithExternalServer(test.test_endtoend.EndToEndTest): + pass + +if __name__ == '__main__': + parser = optparse.OptionParser() + parser.add_option('-p', '--port', dest='port', type='int', + default=_DEFAULT_WEB_SOCKET_PORT, + help='external test server port.') + (options, args) = parser.parse_args() + + test.test_endtoend._use_external_server = True + test.test_endtoend._external_server_port = options.port + + unittest.main(argv=[sys.argv[0]]) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/mock.py b/testing/web-platform/tests/tools/pywebsocket/src/test/mock.py new file mode 100644 index 000000000..6bffcac48 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/mock.py @@ -0,0 +1,221 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Mocks for testing. +""" + + +import Queue +import threading + +from mod_pywebsocket import common +from mod_pywebsocket.stream import StreamHixie75 + + +class _MockConnBase(object): + """Base class of mocks for mod_python.apache.mp_conn. + + This enables tests to check what is written to a (mock) mp_conn. + """ + + def __init__(self): + self._write_data = [] + self.remote_addr = 'fake_address' + + def write(self, data): + """Override mod_python.apache.mp_conn.write.""" + + self._write_data.append(data) + + def written_data(self): + """Get bytes written to this mock.""" + + return ''.join(self._write_data) + + +class MockConn(_MockConnBase): + """Mock for mod_python.apache.mp_conn. + + This enables tests to specify what should be read from a (mock) mp_conn as + well as to check what is written to it. + """ + + def __init__(self, read_data): + """Constructs an instance. + + Args: + read_data: bytes that should be returned when read* methods are + called. + """ + + _MockConnBase.__init__(self) + self._read_data = read_data + self._read_pos = 0 + + def readline(self): + """Override mod_python.apache.mp_conn.readline.""" + + if self._read_pos >= len(self._read_data): + return '' + end_index = self._read_data.find('\n', self._read_pos) + 1 + if not end_index: + end_index = len(self._read_data) + return self._read_up_to(end_index) + + def read(self, length): + """Override mod_python.apache.mp_conn.read.""" + + if self._read_pos >= len(self._read_data): + return '' + end_index = min(len(self._read_data), self._read_pos + length) + return self._read_up_to(end_index) + + def _read_up_to(self, end_index): + line = self._read_data[self._read_pos:end_index] + self._read_pos = end_index + return line + + +class MockBlockingConn(_MockConnBase): + """Blocking mock for mod_python.apache.mp_conn. + + This enables tests to specify what should be read from a (mock) mp_conn as + well as to check what is written to it. + Callers of read* methods will block if there is no bytes available. + """ + + def __init__(self): + _MockConnBase.__init__(self) + self._queue = Queue.Queue() + + def readline(self): + """Override mod_python.apache.mp_conn.readline.""" + line = '' + while True: + c = self._queue.get() + line += c + if c == '\n': + return line + + def read(self, length): + """Override mod_python.apache.mp_conn.read.""" + + data = '' + for unused in range(length): + data += self._queue.get() + return data + + def put_bytes(self, bytes): + """Put bytes to be read from this mock. + + Args: + bytes: bytes to be read. + """ + + for byte in bytes: + self._queue.put(byte) + + +class MockTable(dict): + """Mock table. + + This mimics mod_python mp_table. Note that only the methods used by + tests are overridden. + """ + + def __init__(self, copy_from={}): + if isinstance(copy_from, dict): + copy_from = copy_from.items() + for key, value in copy_from: + self.__setitem__(key, value) + + def __getitem__(self, key): + return super(MockTable, self).__getitem__(key.lower()) + + def __setitem__(self, key, value): + super(MockTable, self).__setitem__(key.lower(), value) + + def get(self, key, def_value=None): + return super(MockTable, self).get(key.lower(), def_value) + + +class MockRequest(object): + """Mock request. + + This mimics mod_python request. + """ + + def __init__(self, uri=None, headers_in={}, connection=None, method='GET', + protocol='HTTP/1.1', is_https=False): + """Construct an instance. + + Arguments: + uri: URI of the request. + headers_in: Request headers. + connection: Connection used for the request. + method: request method. + is_https: Whether this request is over SSL. + + See the document of mod_python Request for details. + """ + self.uri = uri + self.unparsed_uri = uri + self.connection = connection + self.method = method + self.protocol = protocol + self.headers_in = MockTable(headers_in) + # self.is_https_ needs to be accessible from tests. To avoid name + # conflict with self.is_https(), it is named as such. + self.is_https_ = is_https + self.ws_stream = StreamHixie75(self, True) + self.ws_close_code = None + self.ws_close_reason = None + self.ws_version = common.VERSION_HYBI00 + self.ws_deflate = False + + def is_https(self): + """Return whether this request is over SSL.""" + return self.is_https_ + + +class MockDispatcher(object): + """Mock for dispatch.Dispatcher.""" + + def __init__(self): + self.do_extra_handshake_called = False + + def do_extra_handshake(self, conn_context): + self.do_extra_handshake_called = True + + def transfer_data(self, conn_context): + pass + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/mux_client_for_testing.py b/testing/web-platform/tests/tools/pywebsocket/src/test/mux_client_for_testing.py new file mode 100644 index 000000000..dd5435a8c --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/mux_client_for_testing.py @@ -0,0 +1,690 @@ +#!/usr/bin/env python +# +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""WebSocket client utility for testing mux extension. + +This code should be independent from mod_pywebsocket. See the comment of +client_for_testing.py. + +NOTE: This code is far from robust like client_for_testing.py. +""" + + + +import Queue +import base64 +import collections +import email +import email.parser +import logging +import math +import os +import random +import socket +import struct +import threading + +from mod_pywebsocket import util + +from test import client_for_testing + + +_CONTROL_CHANNEL_ID = 0 +_DEFAULT_CHANNEL_ID = 1 + +_MUX_OPCODE_ADD_CHANNEL_REQUEST = 0 +_MUX_OPCODE_ADD_CHANNEL_RESPONSE = 1 +_MUX_OPCODE_FLOW_CONTROL = 2 +_MUX_OPCODE_DROP_CHANNEL = 3 +_MUX_OPCODE_NEW_CHANNEL_SLOT = 4 + + +class _ControlBlock: + def __init__(self, opcode): + self.opcode = opcode + + +def _parse_handshake_response(response): + status_line, header_lines = response.split('\r\n', 1) + + words = status_line.split(' ') + if len(words) < 3: + raise ValueError('Bad Status-Line syntax %r' % status_line) + [version, response_code] = words[:2] + if version != 'HTTP/1.1': + raise ValueError('Bad response version %r' % version) + + if response_code != '101': + raise ValueError('Bad response code %r ' % response_code) + headers = email.parser.Parser().parsestr(header_lines) + return headers + + +def _parse_channel_id(data, offset=0): + length = len(data) + remaining = length - offset + + if remaining <= 0: + raise Exception('No channel id found') + + channel_id = ord(data[offset]) + channel_id_length = 1 + if channel_id & 0xe0 == 0xe0: + if remaining < 4: + raise Exception('Invalid channel id format') + channel_id = struct.unpack('!L', + data[offset:offset+4])[0] & 0x1fffffff + channel_id_length = 4 + elif channel_id & 0xc0 == 0xc0: + if remaining < 3: + raise Exception('Invalid channel id format') + channel_id = (((channel_id & 0x1f) << 16) + + struct.unpack('!H', data[offset+1:offset+3])[0]) + channel_id_length = 3 + elif channel_id & 0x80 == 0x80: + if remaining < 2: + raise Exception('Invalid channel id format') + channel_id = struct.unpack('!H', data[offset:offset+2])[0] & 0x3fff + channel_id_length = 2 + + return channel_id, channel_id_length + + +def _parse_number(data, offset=0): + first_byte = ord(data[offset]) + if (first_byte & 0x80) != 0: + raise Exception('The MSB of number field must be unset') + first_byte = first_byte & 0x7f + if first_byte == 127: + if offset + 9 > len(data): + raise Exception('Invalid number') + return struct.unpack('!Q', data[offset+1:offset+9])[0], 9 + if first_byte == 126: + if offset + 3 > len(data): + raise Exception('Invalid number') + return struct.unpack('!H', data[offset+1:offset+3])[0], 3 + return first_byte, 1 + + +def _parse_size_and_contents(data, offset=0): + size, advance = _parse_number(data, offset) + start_position = offset + advance + end_position = start_position + size + if len(data) < end_position: + raise Exception('Invalid size of control block (%d < %d)' % ( + len(data), end_position)) + return data[start_position:end_position], size + advance + + +def _parse_control_blocks(data): + blocks = [] + length = len(data) + pos = 0 + + while pos < length: + first_byte = ord(data[pos]) + pos += 1 + opcode = (first_byte >> 5) & 0x7 + block = _ControlBlock(opcode) + + # TODO(bashi): Support more opcode + if opcode == _MUX_OPCODE_ADD_CHANNEL_RESPONSE: + block.encode = first_byte & 3 + block.rejected = (first_byte >> 4) & 1 + + channel_id, advance = _parse_channel_id(data, pos) + block.channel_id = channel_id + pos += advance + + encoded_handshake, advance = _parse_size_and_contents(data, pos) + block.encoded_handshake = encoded_handshake + pos += advance + blocks.append(block) + elif opcode == _MUX_OPCODE_DROP_CHANNEL: + block.mux_error = (first_byte >> 4) & 1 + + channel_id, advance = _parse_channel_id(data, pos) + block.channel_id = channel_id + pos += advance + + reason, advance = _parse_size_and_contents(data, pos) + if len(reason) == 0: + block.drop_code = None + block.drop_message = '' + elif len(reason) >= 2: + block.drop_code = struct.unpack('!H', reason[:2])[0] + block.drop_message = reason[2:] + else: + raise Exception('Invalid DropChannel') + pos += advance + blocks.append(block) + elif opcode == _MUX_OPCODE_FLOW_CONTROL: + channel_id, advance = _parse_channel_id(data, pos) + block.channel_id = channel_id + pos += advance + send_quota, advance = _parse_number(data, pos) + block.send_quota = send_quota + pos += advance + blocks.append(block) + elif opcode == _MUX_OPCODE_NEW_CHANNEL_SLOT: + fallback = first_byte & 1 + slots, advance = _parse_number(data, pos) + pos += advance + send_quota, advance = _parse_number(data, pos) + pos += advance + if fallback == 1 and (slots != 0 or send_quota != 0): + raise Exception('slots and send_quota must be zero if F bit ' + 'is set') + block.fallback = fallback + block.slots = slots + block.send_quota = send_quota + blocks.append(block) + else: + raise Exception( + 'Unsupported mux opcode %d received' % opcode) + + return blocks + + +def _encode_channel_id(channel_id): + if channel_id < 0: + raise ValueError('Channel id %d must not be negative' % channel_id) + + if channel_id < 2 ** 7: + return chr(channel_id) + if channel_id < 2 ** 14: + return struct.pack('!H', 0x8000 + channel_id) + if channel_id < 2 ** 21: + first = chr(0xc0 + (channel_id >> 16)) + return first + struct.pack('!H', channel_id & 0xffff) + if channel_id < 2 ** 29: + return struct.pack('!L', 0xe0000000 + channel_id) + + raise ValueError('Channel id %d is too large' % channel_id) + + +def _encode_number(number): + if number <= 125: + return chr(number) + elif number < (1 << 16): + return chr(0x7e) + struct.pack('!H', number) + elif number < (1 << 63): + return chr(0x7f) + struct.pack('!Q', number) + else: + raise Exception('Invalid number') + + +def _create_add_channel_request(channel_id, encoded_handshake, + encoding=0): + length = len(encoded_handshake) + handshake_length = _encode_number(length) + + first_byte = (_MUX_OPCODE_ADD_CHANNEL_REQUEST << 5) | encoding + return (chr(first_byte) + _encode_channel_id(channel_id) + + handshake_length + encoded_handshake) + + +def _create_flow_control(channel_id, replenished_quota): + first_byte = (_MUX_OPCODE_FLOW_CONTROL << 5) + return (chr(first_byte) + _encode_channel_id(channel_id) + + _encode_number(replenished_quota)) + + +class _MuxReaderThread(threading.Thread): + """Mux reader thread. + + Reads frames and passes them to the mux client. This thread accesses + private functions/variables of the mux client. + """ + + def __init__(self, mux): + threading.Thread.__init__(self) + self.setDaemon(True) + self._mux = mux + self._stop_requested = False + + def _receive_message(self): + first_opcode = None + pending_payload = [] + while not self._stop_requested: + fin, rsv1, rsv2, rsv3, opcode, payload_length = ( + client_for_testing.read_frame_header(self._mux._socket)) + + if not first_opcode: + if opcode == client_for_testing.OPCODE_TEXT: + raise Exception('Received a text message on physical ' + 'connection') + if opcode == client_for_testing.OPCODE_CONTINUATION: + raise Exception('Received an intermediate frame but ' + 'fragmentation was not started') + if (opcode == client_for_testing.OPCODE_BINARY or + opcode == client_for_testing.OPCODE_PONG or + opcode == client_for_testing.OPCODE_PONG or + opcode == client_for_testing.OPCODE_CLOSE): + first_opcode = opcode + else: + raise Exception('Received an undefined opcode frame: %d' % + opcode) + + elif opcode != client_for_testing.OPCODE_CONTINUATION: + raise Exception('Received a new opcode before ' + 'terminating fragmentation') + + payload = client_for_testing.receive_bytes( + self._mux._socket, payload_length) + + if self._mux._incoming_frame_filter is not None: + payload = self._mux._incoming_frame_filter.filter(payload) + + pending_payload.append(payload) + + if fin: + break + + if self._stop_requested: + return None, None + + message = ''.join(pending_payload) + return first_opcode, message + + def request_stop(self): + self._stop_requested = True + + def run(self): + try: + while not self._stop_requested: + # opcode is OPCODE_BINARY or control opcodes when a message + # is succesfully received. + opcode, message = self._receive_message() + if not opcode: + return + if opcode == client_for_testing.OPCODE_BINARY: + channel_id, advance = _parse_channel_id(message) + self._mux._dispatch_frame(channel_id, message[advance:]) + else: + self._mux._process_control_message(opcode, message) + finally: + self._mux._notify_reader_done() + + +class _InnerFrame(object): + def __init__(self, fin, rsv1, rsv2, rsv3, opcode, payload): + self.fin = fin + self.rsv1 = rsv1 + self.rsv2 = rsv2 + self.rsv3 = rsv3 + self.opcode = opcode + self.payload = payload + + +class _LogicalChannelData(object): + def __init__(self): + self.queue = Queue.Queue() + self.send_quota = 0 + self.receive_quota = 0 + + +class MuxClient(object): + """WebSocket mux client. + + Note that this class is NOT thread-safe. Do not access an instance of this + class from multiple threads at a same time. + """ + + def __init__(self, options): + self._logger = util.get_class_logger(self) + + self._options = options + self._options.enable_mux() + self._stream = None + self._socket = None + self._handshake = client_for_testing.WebSocketHandshake(self._options) + self._incoming_frame_filter = None + self._outgoing_frame_filter = None + + self._is_active = False + self._read_thread = None + self._control_blocks_condition = threading.Condition() + self._control_blocks = [] + self._channel_slots = collections.deque() + self._logical_channels_condition = threading.Condition(); + self._logical_channels = {} + self._timeout = 2 + self._physical_connection_close_event = None + self._physical_connection_close_message = None + + def _parse_inner_frame(self, data): + if len(data) == 0: + raise Exception('Invalid encapsulated frame received') + + first_byte = ord(data[0]) + fin = (first_byte << 7) & 1 + rsv1 = (first_byte << 6) & 1 + rsv2 = (first_byte << 5) & 1 + rsv3 = (first_byte << 4) & 1 + opcode = first_byte & 0xf + + if self._outgoing_frame_filter: + payload = self._outgoing_frame_filter.filter( + data[1:]) + else: + payload = data[1:] + + return _InnerFrame(fin, rsv1, rsv2, rsv3, opcode, payload) + + def _process_mux_control_blocks(self): + for block in self._control_blocks: + if block.opcode == _MUX_OPCODE_ADD_CHANNEL_RESPONSE: + # AddChannelResponse will be handled in add_channel(). + continue + elif block.opcode == _MUX_OPCODE_FLOW_CONTROL: + try: + self._logical_channels_condition.acquire() + if not block.channel_id in self._logical_channels: + raise Exception('Invalid flow control received for ' + 'channel id %d' % block.channel_id) + self._logical_channels[block.channel_id].send_quota += ( + block.send_quota) + self._logical_channels_condition.notify() + finally: + self._logical_channels_condition.release() + elif block.opcode == _MUX_OPCODE_NEW_CHANNEL_SLOT: + self._channel_slots.extend([block.send_quota] * block.slots) + + def _dispatch_frame(self, channel_id, payload): + if channel_id == _CONTROL_CHANNEL_ID: + try: + self._control_blocks_condition.acquire() + self._control_blocks += _parse_control_blocks(payload) + self._process_mux_control_blocks() + self._control_blocks_condition.notify() + finally: + self._control_blocks_condition.release() + else: + try: + self._logical_channels_condition.acquire() + if not channel_id in self._logical_channels: + raise Exception('Received logical frame on channel id ' + '%d, which is not established' % + channel_id) + + inner_frame = self._parse_inner_frame(payload) + self._logical_channels[channel_id].receive_quota -= ( + len(inner_frame.payload)) + if self._logical_channels[channel_id].receive_quota < 0: + raise Exception('The server violates quota on ' + 'channel id %d' % channel_id) + finally: + self._logical_channels_condition.release() + self._logical_channels[channel_id].queue.put(inner_frame) + + def _process_control_message(self, opcode, message): + # Ping/Pong are not supported. + if opcode == client_for_testing.OPCODE_CLOSE: + self._physical_connection_close_message = message + if self._is_active: + self._stream.send_close( + code=client_for_testing.STATUS_NORMAL_CLOSURE, reason='') + self._read_thread.request_stop() + + if self._physical_connection_close_event: + self._physical_connection_close_event.set() + + def _notify_reader_done(self): + self._logger.debug('Read thread terminated.') + self.close_socket() + + def _assert_channel_slot_available(self): + try: + self._control_blocks_condition.acquire() + if len(self._channel_slots) == 0: + # Wait once + self._control_blocks_condition.wait(timeout=self._timeout) + finally: + self._control_blocks_condition.release() + + if len(self._channel_slots) == 0: + raise Exception('Failed to receive NewChannelSlot') + + def _assert_send_quota_available(self, channel_id): + try: + self._logical_channels_condition.acquire() + if self._logical_channels[channel_id].send_quota == 0: + # Wait once + self._logical_channels_condition.wait(timeout=self._timeout) + finally: + self._logical_channels_condition.release() + + if self._logical_channels[channel_id].send_quota == 0: + raise Exception('Failed to receive FlowControl for channel id %d' % + channel_id) + + def connect(self): + self._socket = client_for_testing.connect_socket_with_retry( + self._options.server_host, + self._options.server_port, + self._options.socket_timeout, + self._options.use_tls) + + self._handshake.handshake(self._socket) + self._stream = client_for_testing.WebSocketStream( + self._socket, self._handshake) + + self._logical_channels[_DEFAULT_CHANNEL_ID] = _LogicalChannelData() + + self._read_thread = _MuxReaderThread(self) + self._read_thread.start() + + self._assert_channel_slot_available() + self._assert_send_quota_available(_DEFAULT_CHANNEL_ID) + + self._is_active = True + self._logger.info('Connection established') + + def add_channel(self, channel_id, options): + if not self._is_active: + raise Exception('Mux client is not active') + + if channel_id in self._logical_channels: + raise Exception('Channel id %d already exists' % channel_id) + + try: + send_quota = self._channel_slots.popleft() + except IndexError, e: + raise Exception('No channel slots: %r' % e) + + # Create AddChannel request + request_line = 'GET %s HTTP/1.1\r\n' % options.resource + fields = [] + if options.server_port == client_for_testing.DEFAULT_PORT: + fields.append('Host: %s\r\n' % options.server_host.lower()) + else: + fields.append('Host: %s:%d\r\n' % (options.server_host.lower(), + options.server_port)) + fields.append('Origin: %s\r\n' % options.origin.lower()) + fields.append('Connection: Upgrade\r\n') + + if len(options.extensions) > 0: + fields.append('Sec-WebSocket-Extensions: %s\r\n' % + ', '.join(options.extensions)) + + handshake = request_line + ''.join(fields) + '\r\n' + add_channel_request = _create_add_channel_request( + channel_id, handshake) + payload = _encode_channel_id(_CONTROL_CHANNEL_ID) + add_channel_request + self._stream.send_binary(payload) + + # Wait AddChannelResponse + self._logger.debug('Waiting AddChannelResponse for the request...') + response = None + try: + self._control_blocks_condition.acquire() + while True: + for block in self._control_blocks: + if block.opcode != _MUX_OPCODE_ADD_CHANNEL_RESPONSE: + continue + if block.channel_id == channel_id: + response = block + self._control_blocks.remove(response) + break + if response: + break + self._control_blocks_condition.wait(self._timeout) + if not self._is_active: + raise Exception('AddChannelRequest timed out') + finally: + self._control_blocks_condition.release() + + # Validate AddChannelResponse + if response.rejected: + raise Exception('The server rejected AddChannelRequest') + + fields = _parse_handshake_response(response.encoded_handshake) + + # Should we reject when Upgrade, Connection, or Sec-WebSocket-Accept + # headers exist? + + self._logical_channels_condition.acquire() + self._logical_channels[channel_id] = _LogicalChannelData() + self._logical_channels[channel_id].send_quota = send_quota + self._logical_channels_condition.release() + + self._logger.debug('Logical channel %d established' % channel_id) + + def _check_logical_channel_is_opened(self, channel_id): + if not self._is_active: + raise Exception('Mux client is not active') + + if not channel_id in self._logical_channels: + raise Exception('Logical channel %d is not established.') + + def drop_channel(self, channel_id): + # TODO(bashi): Implement + pass + + def send_flow_control(self, channel_id, replenished_quota): + self._check_logical_channel_is_opened(channel_id) + flow_control = _create_flow_control(channel_id, replenished_quota) + payload = _encode_channel_id(_CONTROL_CHANNEL_ID) + flow_control + # Replenish receive quota + try: + self._logical_channels_condition.acquire() + self._logical_channels[channel_id].receive_quota += ( + replenished_quota) + finally: + self._logical_channels_condition.release() + self._stream.send_binary(payload) + + def send_message(self, channel_id, message, end=True, binary=False): + self._check_logical_channel_is_opened(channel_id) + + if binary: + first_byte = (end << 7) | client_for_testing.OPCODE_BINARY + else: + first_byte = (end << 7) | client_for_testing.OPCODE_TEXT + message = message.encode('utf-8') + + try: + self._logical_channels_condition.acquire() + if self._logical_channels[channel_id].send_quota < len(message): + raise Exception('Send quota violation: %d < %d' % ( + self._logical_channels[channel_id].send_quota, + len(message))) + + self._logical_channels[channel_id].send_quota -= len(message) + finally: + self._logical_channels_condition.release() + payload = _encode_channel_id(channel_id) + chr(first_byte) + message + self._stream.send_binary(payload) + + def assert_receive(self, channel_id, payload, binary=False): + self._check_logical_channel_is_opened(channel_id) + + try: + inner_frame = self._logical_channels[channel_id].queue.get( + timeout=self._timeout) + except Queue.Empty, e: + raise Exception('Cannot receive message from channel id %d' % + channel_id) + + if binary: + opcode = client_for_testing.OPCODE_BINARY + else: + opcode = client_for_testing.OPCODE_TEXT + + if inner_frame.opcode != opcode: + raise Exception('Unexpected opcode received (%r != %r)' % + (expected_opcode, inner_frame.opcode)) + + if inner_frame.payload != payload: + raise Exception('Unexpected payload received') + + def send_close(self, channel_id, code=None, reason=''): + self._check_logical_channel_is_opened(channel_id) + + if code is not None: + body = struct.pack('!H', code) + reason.encode('utf-8') + else: + body = '' + + first_byte = (1 << 7) | client_for_testing.OPCODE_CLOSE + payload = _encode_channel_id(channel_id) + chr(first_byte) + body + self._stream.send_binary(payload) + + def assert_receive_close(self, channel_id): + self._check_logical_channel_is_opened(channel_id) + + try: + inner_frame = self._logical_channels[channel_id].queue.get( + timeout=self._timeout) + except Queue.Empty, e: + raise Exception('Cannot receive message from channel id %d' % + channel_id) + if inner_frame.opcode != client_for_testing.OPCODE_CLOSE: + raise Exception('Didn\'t receive close frame') + + def send_physical_connection_close(self, code=None, reason=''): + self._physical_connection_close_event = threading.Event() + self._stream.send_close(code, reason) + + # This method can be used only after calling + # send_physical_connection_close(). + def assert_physical_connection_receive_close( + self, code=client_for_testing.STATUS_NORMAL_CLOSURE, reason=''): + self._physical_connection_close_event.wait(timeout=self._timeout) + if (not self._physical_connection_close_event.isSet() or + not self._physical_connection_close_message): + raise Exception('Didn\'t receive closing handshake') + + def close_socket(self): + self._is_active = False + self._socket.close() diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/run_all.py b/testing/web-platform/tests/tools/pywebsocket/src/test/run_all.py new file mode 100755 index 000000000..80a5d87d8 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/run_all.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Run all tests in the same directory. + +This suite is expected to be run under pywebsocket's src directory, i.e. the +directory containing mod_pywebsocket, test, etc. + +To change loggin level, please specify --log-level option. + python test/run_test.py --log-level debug + +To pass any option to unittest module, please specify options after '--'. For +example, run this for making the test runner verbose. + python test/run_test.py --log-level debug -- -v +""" + + +import logging +import optparse +import os +import re +import sys +import unittest + + +_TEST_MODULE_PATTERN = re.compile(r'^(test_.+)\.py$') + + +def _list_test_modules(directory): + module_names = [] + for filename in os.listdir(directory): + match = _TEST_MODULE_PATTERN.search(filename) + if match: + module_names.append(match.group(1)) + return module_names + + +def _suite(): + loader = unittest.TestLoader() + return loader.loadTestsFromNames( + _list_test_modules(os.path.join(os.path.split(__file__)[0], '.'))) + + +if __name__ == '__main__': + parser = optparse.OptionParser() + parser.add_option('--log-level', '--log_level', type='choice', + dest='log_level', default='warning', + choices=['debug', 'info', 'warning', 'warn', 'error', + 'critical']) + options, args = parser.parse_args() + logging.basicConfig( + level=logging.getLevelName(options.log_level.upper()), + format='%(levelname)s %(asctime)s ' + '%(filename)s:%(lineno)d] ' + '%(message)s', + datefmt='%H:%M:%S') + unittest.main(defaultTest='_suite', argv=[sys.argv[0]] + args) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/set_sys_path.py b/testing/web-platform/tests/tools/pywebsocket/src/test/set_sys_path.py new file mode 100644 index 000000000..e3c6db9ea --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/set_sys_path.py @@ -0,0 +1,45 @@ +# Copyright 2009, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Configuration for testing. + +Test files should import this module before mod_pywebsocket. +""" + + +import os +import sys + + +# Add the parent directory to sys.path to enable importing mod_pywebsocket. +sys.path.insert(0, os.path.join(os.path.split(__file__)[0], '..')) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_dispatch.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_dispatch.py new file mode 100755 index 000000000..9ca3d4f3a --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_dispatch.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python +# +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Tests for dispatch module.""" + + +import os +import unittest + +import set_sys_path # Update sys.path to locate mod_pywebsocket module. + +from mod_pywebsocket import dispatch +from mod_pywebsocket import handshake +from test import mock + + +_TEST_HANDLERS_DIR = os.path.join( + os.path.split(__file__)[0], 'testdata', 'handlers') + +_TEST_HANDLERS_SUB_DIR = os.path.join(_TEST_HANDLERS_DIR, 'sub') + + +class DispatcherTest(unittest.TestCase): + """A unittest for dispatch module.""" + + def test_normalize_path(self): + self.assertEqual(os.path.abspath('/a/b').replace('\\', '/'), + dispatch._normalize_path('/a/b')) + self.assertEqual(os.path.abspath('/a/b').replace('\\', '/'), + dispatch._normalize_path('\\a\\b')) + self.assertEqual(os.path.abspath('/a/b').replace('\\', '/'), + dispatch._normalize_path('/a/c/../b')) + self.assertEqual(os.path.abspath('abc').replace('\\', '/'), + dispatch._normalize_path('abc')) + + def test_converter(self): + converter = dispatch._create_path_to_resource_converter('/a/b') + # Python built by MSC inserts a drive name like 'C:\' via realpath(). + # Converter Generator expands provided path using realpath() and uses + # the path including a drive name to verify the prefix. + os_root = os.path.realpath('/') + self.assertEqual('/h', converter(os_root + 'a/b/h_wsh.py')) + self.assertEqual('/c/h', converter(os_root + 'a/b/c/h_wsh.py')) + self.assertEqual(None, converter(os_root + 'a/b/h.py')) + self.assertEqual(None, converter('a/b/h_wsh.py')) + + converter = dispatch._create_path_to_resource_converter('a/b') + self.assertEqual('/h', converter(dispatch._normalize_path( + 'a/b/h_wsh.py'))) + + converter = dispatch._create_path_to_resource_converter('/a/b///') + self.assertEqual('/h', converter(os_root + 'a/b/h_wsh.py')) + self.assertEqual('/h', converter(dispatch._normalize_path( + '/a/b/../b/h_wsh.py'))) + + converter = dispatch._create_path_to_resource_converter( + '/a/../a/b/../b/') + self.assertEqual('/h', converter(os_root + 'a/b/h_wsh.py')) + + converter = dispatch._create_path_to_resource_converter(r'\a\b') + self.assertEqual('/h', converter(os_root + r'a\b\h_wsh.py')) + self.assertEqual('/h', converter(os_root + r'a/b/h_wsh.py')) + + def test_enumerate_handler_file_paths(self): + paths = list( + dispatch._enumerate_handler_file_paths(_TEST_HANDLERS_DIR)) + paths.sort() + self.assertEqual(8, len(paths)) + expected_paths = [ + os.path.join(_TEST_HANDLERS_DIR, 'abort_by_user_wsh.py'), + os.path.join(_TEST_HANDLERS_DIR, 'blank_wsh.py'), + os.path.join(_TEST_HANDLERS_DIR, 'origin_check_wsh.py'), + os.path.join(_TEST_HANDLERS_DIR, 'sub', + 'exception_in_transfer_wsh.py'), + os.path.join(_TEST_HANDLERS_DIR, 'sub', 'non_callable_wsh.py'), + os.path.join(_TEST_HANDLERS_DIR, 'sub', 'plain_wsh.py'), + os.path.join(_TEST_HANDLERS_DIR, 'sub', + 'wrong_handshake_sig_wsh.py'), + os.path.join(_TEST_HANDLERS_DIR, 'sub', + 'wrong_transfer_sig_wsh.py'), + ] + for expected, actual in zip(expected_paths, paths): + self.assertEqual(expected, actual) + + def test_source_handler_file(self): + self.assertRaises( + dispatch.DispatchException, dispatch._source_handler_file, '') + self.assertRaises( + dispatch.DispatchException, dispatch._source_handler_file, 'def') + self.assertRaises( + dispatch.DispatchException, dispatch._source_handler_file, '1/0') + self.failUnless(dispatch._source_handler_file( + 'def web_socket_do_extra_handshake(request):pass\n' + 'def web_socket_transfer_data(request):pass\n')) + + def test_source_warnings(self): + dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None) + warnings = dispatcher.source_warnings() + warnings.sort() + expected_warnings = [ + (os.path.realpath(os.path.join( + _TEST_HANDLERS_DIR, 'blank_wsh.py')) + + ': web_socket_do_extra_handshake is not defined.'), + (os.path.realpath(os.path.join( + _TEST_HANDLERS_DIR, 'sub', 'non_callable_wsh.py')) + + ': web_socket_do_extra_handshake is not callable.'), + (os.path.realpath(os.path.join( + _TEST_HANDLERS_DIR, 'sub', 'wrong_handshake_sig_wsh.py')) + + ': web_socket_do_extra_handshake is not defined.'), + (os.path.realpath(os.path.join( + _TEST_HANDLERS_DIR, 'sub', 'wrong_transfer_sig_wsh.py')) + + ': web_socket_transfer_data is not defined.'), + ] + self.assertEquals(4, len(warnings)) + for expected, actual in zip(expected_warnings, warnings): + self.assertEquals(expected, actual) + + def test_do_extra_handshake(self): + dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None) + request = mock.MockRequest() + request.ws_resource = '/origin_check' + request.ws_origin = 'http://example.com' + dispatcher.do_extra_handshake(request) # Must not raise exception. + + request.ws_origin = 'http://bad.example.com' + try: + dispatcher.do_extra_handshake(request) + self.fail('Could not catch HandshakeException with 403 status') + except handshake.HandshakeException, e: + self.assertEquals(403, e.status) + except Exception, e: + self.fail('Unexpected exception: %r' % e) + + def test_abort_extra_handshake(self): + dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None) + request = mock.MockRequest() + request.ws_resource = '/abort_by_user' + self.assertRaises(handshake.AbortedByUserException, + dispatcher.do_extra_handshake, request) + + def test_transfer_data(self): + dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None) + + request = mock.MockRequest(connection=mock.MockConn('\xff\x00')) + request.ws_resource = '/origin_check' + request.ws_protocol = 'p1' + dispatcher.transfer_data(request) + self.assertEqual('origin_check_wsh.py is called for /origin_check, p1' + '\xff\x00', + request.connection.written_data()) + + request = mock.MockRequest(connection=mock.MockConn('\xff\x00')) + request.ws_resource = '/sub/plain' + request.ws_protocol = None + dispatcher.transfer_data(request) + self.assertEqual('sub/plain_wsh.py is called for /sub/plain, None' + '\xff\x00', + request.connection.written_data()) + + request = mock.MockRequest(connection=mock.MockConn('\xff\x00')) + request.ws_resource = '/sub/plain?' + request.ws_protocol = None + dispatcher.transfer_data(request) + self.assertEqual('sub/plain_wsh.py is called for /sub/plain?, None' + '\xff\x00', + request.connection.written_data()) + + request = mock.MockRequest(connection=mock.MockConn('\xff\x00')) + request.ws_resource = '/sub/plain?q=v' + request.ws_protocol = None + dispatcher.transfer_data(request) + self.assertEqual('sub/plain_wsh.py is called for /sub/plain?q=v, None' + '\xff\x00', + request.connection.written_data()) + + def test_transfer_data_no_handler(self): + dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None) + for resource in ['/blank', '/sub/non_callable', + '/sub/no_wsh_at_the_end', '/does/not/exist']: + request = mock.MockRequest(connection=mock.MockConn('')) + request.ws_resource = resource + request.ws_protocol = 'p2' + try: + dispatcher.transfer_data(request) + self.fail() + except dispatch.DispatchException, e: + self.failUnless(str(e).find('No handler') != -1) + except Exception: + self.fail() + + def test_transfer_data_handler_exception(self): + dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None) + request = mock.MockRequest(connection=mock.MockConn('')) + request.ws_resource = '/sub/exception_in_transfer' + request.ws_protocol = 'p3' + try: + dispatcher.transfer_data(request) + self.fail() + except Exception, e: + self.failUnless(str(e).find('Intentional') != -1, + 'Unexpected exception: %s' % e) + + def test_abort_transfer_data(self): + dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None) + request = mock.MockRequest() + request.ws_resource = '/abort_by_user' + self.assertRaises(handshake.AbortedByUserException, + dispatcher.transfer_data, request) + + def test_scan_dir(self): + disp = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None) + self.assertEqual(4, len(disp._handler_suite_map)) + self.failUnless('/origin_check' in disp._handler_suite_map) + self.failUnless( + '/sub/exception_in_transfer' in disp._handler_suite_map) + self.failUnless('/sub/plain' in disp._handler_suite_map) + + def test_scan_sub_dir(self): + disp = dispatch.Dispatcher(_TEST_HANDLERS_DIR, _TEST_HANDLERS_SUB_DIR) + self.assertEqual(2, len(disp._handler_suite_map)) + self.failIf('/origin_check' in disp._handler_suite_map) + self.failUnless( + '/sub/exception_in_transfer' in disp._handler_suite_map) + self.failUnless('/sub/plain' in disp._handler_suite_map) + + def test_scan_sub_dir_as_root(self): + disp = dispatch.Dispatcher(_TEST_HANDLERS_SUB_DIR, + _TEST_HANDLERS_SUB_DIR) + self.assertEqual(2, len(disp._handler_suite_map)) + self.failIf('/origin_check' in disp._handler_suite_map) + self.failIf('/sub/exception_in_transfer' in disp._handler_suite_map) + self.failIf('/sub/plain' in disp._handler_suite_map) + self.failUnless('/exception_in_transfer' in disp._handler_suite_map) + self.failUnless('/plain' in disp._handler_suite_map) + + def test_scan_dir_must_under_root(self): + dispatch.Dispatcher('a/b', 'a/b/c') # OK + dispatch.Dispatcher('a/b///', 'a/b') # OK + self.assertRaises(dispatch.DispatchException, + dispatch.Dispatcher, 'a/b/c', 'a/b') + + def test_resource_path_alias(self): + disp = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None) + disp.add_resource_path_alias('/', '/origin_check') + self.assertEqual(5, len(disp._handler_suite_map)) + self.failUnless('/origin_check' in disp._handler_suite_map) + self.failUnless( + '/sub/exception_in_transfer' in disp._handler_suite_map) + self.failUnless('/sub/plain' in disp._handler_suite_map) + self.failUnless('/' in disp._handler_suite_map) + self.assertRaises(dispatch.DispatchException, + disp.add_resource_path_alias, '/alias', '/not-exist') + + +if __name__ == '__main__': + unittest.main() + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_endtoend.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_endtoend.py new file mode 100755 index 000000000..5e5cf6157 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_endtoend.py @@ -0,0 +1,753 @@ +#!/usr/bin/env python +# +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""End-to-end tests for pywebsocket. Tests standalone.py by default. You +can also test mod_pywebsocket hosted on an Apache server by setting +_use_external_server to True and modifying _external_server_port to point to +the port on which the Apache server is running. +""" + + +import logging +import os +import signal +import socket +import subprocess +import sys +import time +import unittest + +import set_sys_path # Update sys.path to locate mod_pywebsocket module. + +from test import client_for_testing +from test import mux_client_for_testing + + +# Special message that tells the echo server to start closing handshake +_GOODBYE_MESSAGE = 'Goodbye' + +_SERVER_WARMUP_IN_SEC = 0.2 + +# If you want to use external server to run end to end tests, set following +# parameters correctly. +_use_external_server = False +_external_server_port = 0 + + +# Test body functions +def _echo_check_procedure(client): + client.connect() + + client.send_message('test') + client.assert_receive('test') + client.send_message('helloworld') + client.assert_receive('helloworld') + + client.send_close() + client.assert_receive_close() + + client.assert_connection_closed() + + +def _echo_check_procedure_with_binary(client): + client.connect() + + client.send_message('binary', binary=True) + client.assert_receive('binary', binary=True) + client.send_message('\x00\x80\xfe\xff\x00\x80', binary=True) + client.assert_receive('\x00\x80\xfe\xff\x00\x80', binary=True) + + client.send_close() + client.assert_receive_close() + + client.assert_connection_closed() + + +def _echo_check_procedure_with_goodbye(client): + client.connect() + + client.send_message('test') + client.assert_receive('test') + + client.send_message(_GOODBYE_MESSAGE) + client.assert_receive(_GOODBYE_MESSAGE) + + client.assert_receive_close() + client.send_close() + + client.assert_connection_closed() + + +def _echo_check_procedure_with_code_and_reason(client, code, reason): + client.connect() + + client.send_close(code, reason) + client.assert_receive_close(code, reason) + + client.assert_connection_closed() + + +def _unmasked_frame_check_procedure(client): + client.connect() + + client.send_message('test', mask=False) + client.assert_receive_close(client_for_testing.STATUS_PROTOCOL_ERROR, '') + + client.assert_connection_closed() + + +def _mux_echo_check_procedure(mux_client): + mux_client.connect() + mux_client.send_flow_control(1, 1024) + + logical_channel_options = client_for_testing.ClientOptions() + logical_channel_options.server_host = 'localhost' + logical_channel_options.server_port = 80 + logical_channel_options.origin = 'http://localhost' + logical_channel_options.resource = '/echo' + mux_client.add_channel(2, logical_channel_options) + mux_client.send_flow_control(2, 1024) + + mux_client.send_message(2, 'test') + mux_client.assert_receive(2, 'test') + + mux_client.add_channel(3, logical_channel_options) + mux_client.send_flow_control(3, 1024) + + mux_client.send_message(2, 'hello') + mux_client.send_message(3, 'world') + mux_client.assert_receive(2, 'hello') + mux_client.assert_receive(3, 'world') + + # Don't send close message on channel id 1 so that server-initiated + # closing handshake won't occur. + mux_client.send_close(2) + mux_client.send_close(3) + mux_client.assert_receive_close(2) + mux_client.assert_receive_close(3) + + mux_client.send_physical_connection_close() + mux_client.assert_physical_connection_receive_close() + + +class EndToEndTestBase(unittest.TestCase): + """Base class for end-to-end tests that launch pywebsocket standalone + server as a separate process, connect to it using the client_for_testing + module, and check if the server behaves correctly by exchanging opening + handshake and frames over a TCP connection. + """ + + def setUp(self): + self.server_stderr = None + self.top_dir = os.path.join(os.path.split(__file__)[0], '..') + os.putenv('PYTHONPATH', os.path.pathsep.join(sys.path)) + self.standalone_command = os.path.join( + self.top_dir, 'mod_pywebsocket', 'standalone.py') + self.document_root = os.path.join(self.top_dir, 'example') + s = socket.socket() + s.bind(('localhost', 0)) + (_, self.test_port) = s.getsockname() + s.close() + + self._options = client_for_testing.ClientOptions() + self._options.server_host = 'localhost' + self._options.origin = 'http://localhost' + self._options.resource = '/echo' + + # TODO(toyoshim): Eliminate launching a standalone server on using + # external server. + + if _use_external_server: + self._options.server_port = _external_server_port + else: + self._options.server_port = self.test_port + + # TODO(tyoshino): Use tearDown to kill the server. + + def _run_python_command(self, commandline, stdout=None, stderr=None): + return subprocess.Popen([sys.executable] + commandline, close_fds=True, + stdout=stdout, stderr=stderr) + + def _run_server(self): + args = [self.standalone_command, + '-H', 'localhost', + '-V', 'localhost', + '-p', str(self.test_port), + '-P', str(self.test_port), + '-d', self.document_root] + + # Inherit the level set to the root logger by test runner. + root_logger = logging.getLogger() + log_level = root_logger.getEffectiveLevel() + if log_level != logging.NOTSET: + args.append('--log-level') + args.append(logging.getLevelName(log_level).lower()) + + return self._run_python_command(args, + stderr=self.server_stderr) + + def _kill_process(self, pid): + if sys.platform in ('win32', 'cygwin'): + subprocess.call( + ('taskkill.exe', '/f', '/pid', str(pid)), close_fds=True) + else: + os.kill(pid, signal.SIGKILL) + + +class EndToEndHyBiTest(EndToEndTestBase): + def setUp(self): + EndToEndTestBase.setUp(self) + + def _run_test_with_client_options(self, test_function, options): + server = self._run_server() + try: + # TODO(tyoshino): add some logic to poll the server until it + # becomes ready + time.sleep(_SERVER_WARMUP_IN_SEC) + + client = client_for_testing.create_client(options) + try: + test_function(client) + finally: + client.close_socket() + finally: + self._kill_process(server.pid) + + def _run_test(self, test_function): + self._run_test_with_client_options(test_function, self._options) + + def _run_deflate_frame_test(self, test_function): + server = self._run_server() + try: + time.sleep(_SERVER_WARMUP_IN_SEC) + + self._options.enable_deflate_frame() + client = client_for_testing.create_client(self._options) + try: + test_function(client) + finally: + client.close_socket() + finally: + self._kill_process(server.pid) + + def _run_permessage_deflate_test( + self, offer, response_checker, test_function): + server = self._run_server() + try: + time.sleep(_SERVER_WARMUP_IN_SEC) + + self._options.extensions += offer + self._options.check_permessage_deflate = response_checker + client = client_for_testing.create_client(self._options) + + try: + client.connect() + + if test_function is not None: + test_function(client) + + client.assert_connection_closed() + finally: + client.close_socket() + finally: + self._kill_process(server.pid) + + def _run_close_with_code_and_reason_test(self, test_function, code, + reason): + server = self._run_server() + try: + time.sleep(_SERVER_WARMUP_IN_SEC) + + client = client_for_testing.create_client(self._options) + try: + test_function(client, code, reason) + finally: + client.close_socket() + finally: + self._kill_process(server.pid) + + def _run_http_fallback_test(self, options, status): + server = self._run_server() + try: + time.sleep(_SERVER_WARMUP_IN_SEC) + + client = client_for_testing.create_client(options) + try: + client.connect() + self.fail('Could not catch HttpStatusException') + except client_for_testing.HttpStatusException, e: + self.assertEqual(status, e.status) + except Exception, e: + self.fail('Catch unexpected exception') + finally: + client.close_socket() + finally: + self._kill_process(server.pid) + + def _run_mux_test(self, test_function): + server = self._run_server() + try: + time.sleep(_SERVER_WARMUP_IN_SEC) + + client = mux_client_for_testing.MuxClient(self._options) + try: + test_function(client) + finally: + client.close_socket() + finally: + self._kill_process(server.pid) + + def test_echo(self): + self._run_test(_echo_check_procedure) + + def test_echo_binary(self): + self._run_test(_echo_check_procedure_with_binary) + + def test_echo_server_close(self): + self._run_test(_echo_check_procedure_with_goodbye) + + def test_unmasked_frame(self): + self._run_test(_unmasked_frame_check_procedure) + + def test_echo_deflate_frame(self): + self._run_deflate_frame_test(_echo_check_procedure) + + def test_echo_deflate_frame_server_close(self): + self._run_deflate_frame_test( + _echo_check_procedure_with_goodbye) + + def test_echo_permessage_deflate(self): + def test_function(client): + # From the examples in the spec. + compressed_hello = '\xf2\x48\xcd\xc9\xc9\x07\x00' + client._stream.send_data( + compressed_hello, + client_for_testing.OPCODE_TEXT, + rsv1=1) + client._stream.assert_receive_binary( + compressed_hello, + opcode=client_for_testing.OPCODE_TEXT, + rsv1=1) + + client.send_close() + client.assert_receive_close() + + def response_checker(parameter): + self.assertEquals('permessage-deflate', parameter.name()) + self.assertEquals([], parameter.get_parameters()) + + self._run_permessage_deflate_test( + ['permessage-deflate'], + response_checker, + test_function) + + def test_echo_permessage_deflate_two_frames(self): + def test_function(client): + # From the examples in the spec. + client._stream.send_data( + '\xf2\x48\xcd', + client_for_testing.OPCODE_TEXT, + end=False, + rsv1=1) + client._stream.send_data( + '\xc9\xc9\x07\x00', + client_for_testing.OPCODE_TEXT) + client._stream.assert_receive_binary( + '\xf2\x48\xcd\xc9\xc9\x07\x00', + opcode=client_for_testing.OPCODE_TEXT, + rsv1=1) + + client.send_close() + client.assert_receive_close() + + def response_checker(parameter): + self.assertEquals('permessage-deflate', parameter.name()) + self.assertEquals([], parameter.get_parameters()) + + self._run_permessage_deflate_test( + ['permessage-deflate'], + response_checker, + test_function) + + def test_echo_permessage_deflate_two_messages(self): + def test_function(client): + # From the examples in the spec. + client._stream.send_data( + '\xf2\x48\xcd\xc9\xc9\x07\x00', + client_for_testing.OPCODE_TEXT, + rsv1=1) + client._stream.send_data( + '\xf2\x00\x11\x00\x00', + client_for_testing.OPCODE_TEXT, + rsv1=1) + client._stream.assert_receive_binary( + '\xf2\x48\xcd\xc9\xc9\x07\x00', + opcode=client_for_testing.OPCODE_TEXT, + rsv1=1) + client._stream.assert_receive_binary( + '\xf2\x00\x11\x00\x00', + opcode=client_for_testing.OPCODE_TEXT, + rsv1=1) + + client.send_close() + client.assert_receive_close() + + def response_checker(parameter): + self.assertEquals('permessage-deflate', parameter.name()) + self.assertEquals([], parameter.get_parameters()) + + self._run_permessage_deflate_test( + ['permessage-deflate'], + response_checker, + test_function) + + def test_echo_permessage_deflate_two_msgs_server_no_context_takeover(self): + def test_function(client): + # From the examples in the spec. + client._stream.send_data( + '\xf2\x48\xcd\xc9\xc9\x07\x00', + client_for_testing.OPCODE_TEXT, + rsv1=1) + client._stream.send_data( + '\xf2\x00\x11\x00\x00', + client_for_testing.OPCODE_TEXT, + rsv1=1) + client._stream.assert_receive_binary( + '\xf2\x48\xcd\xc9\xc9\x07\x00', + opcode=client_for_testing.OPCODE_TEXT, + rsv1=1) + client._stream.assert_receive_binary( + '\xf2\x48\xcd\xc9\xc9\x07\x00', + opcode=client_for_testing.OPCODE_TEXT, + rsv1=1) + + client.send_close() + client.assert_receive_close() + + def response_checker(parameter): + self.assertEquals('permessage-deflate', parameter.name()) + self.assertEquals([('server_no_context_takeover', None)], + parameter.get_parameters()) + + self._run_permessage_deflate_test( + ['permessage-deflate; server_no_context_takeover'], + response_checker, + test_function) + + def test_echo_permessage_deflate_preference(self): + def test_function(client): + # From the examples in the spec. + compressed_hello = '\xf2\x48\xcd\xc9\xc9\x07\x00' + client._stream.send_data( + compressed_hello, + client_for_testing.OPCODE_TEXT, + rsv1=1) + client._stream.assert_receive_binary( + compressed_hello, + opcode=client_for_testing.OPCODE_TEXT, + rsv1=1) + + client.send_close() + client.assert_receive_close() + + def response_checker(parameter): + self.assertEquals('permessage-deflate', parameter.name()) + self.assertEquals([], parameter.get_parameters()) + + self._run_permessage_deflate_test( + ['permessage-deflate', 'deflate-frame'], + response_checker, + test_function) + + def test_echo_permessage_deflate_with_parameters(self): + def test_function(client): + # From the examples in the spec. + compressed_hello = '\xf2\x48\xcd\xc9\xc9\x07\x00' + client._stream.send_data( + compressed_hello, + client_for_testing.OPCODE_TEXT, + rsv1=1) + client._stream.assert_receive_binary( + compressed_hello, + opcode=client_for_testing.OPCODE_TEXT, + rsv1=1) + + client.send_close() + client.assert_receive_close() + + def response_checker(parameter): + self.assertEquals('permessage-deflate', parameter.name()) + self.assertEquals([('server_max_window_bits', '10'), + ('server_no_context_takeover', None)], + parameter.get_parameters()) + + self._run_permessage_deflate_test( + ['permessage-deflate; server_max_window_bits=10; ' + 'server_no_context_takeover'], + response_checker, + test_function) + + def test_echo_permessage_deflate_with_bad_server_max_window_bits(self): + def test_function(client): + client.send_close() + client.assert_receive_close() + + def response_checker(parameter): + raise Exception('Unexpected acceptance of permessage-deflate') + + self._run_permessage_deflate_test( + ['permessage-deflate; server_max_window_bits=3000000'], + response_checker, + test_function) + + def test_echo_permessage_deflate_with_bad_server_max_window_bits(self): + def test_function(client): + client.send_close() + client.assert_receive_close() + + def response_checker(parameter): + raise Exception('Unexpected acceptance of permessage-deflate') + + self._run_permessage_deflate_test( + ['permessage-deflate; server_max_window_bits=3000000'], + response_checker, + test_function) + + def test_echo_permessage_deflate_with_undefined_parameter(self): + def test_function(client): + client.send_close() + client.assert_receive_close() + + def response_checker(parameter): + raise Exception('Unexpected acceptance of permessage-deflate') + + self._run_permessage_deflate_test( + ['permessage-deflate; foo=bar'], + response_checker, + test_function) + + def test_echo_close_with_code_and_reason(self): + self._options.resource = '/close' + self._run_close_with_code_and_reason_test( + _echo_check_procedure_with_code_and_reason, 3333, 'sunsunsunsun') + + def test_echo_close_with_empty_body(self): + self._options.resource = '/close' + self._run_close_with_code_and_reason_test( + _echo_check_procedure_with_code_and_reason, None, '') + + def test_mux_echo(self): + self._run_mux_test(_mux_echo_check_procedure) + + def test_close_on_protocol_error(self): + """Tests that the server sends a close frame with protocol error status + code when the client sends data with some protocol error. + """ + + def test_function(client): + client.connect() + + # Intermediate frame without any preceding start of fragmentation + # frame. + client.send_frame_of_arbitrary_bytes('\x80\x80', '') + client.assert_receive_close( + client_for_testing.STATUS_PROTOCOL_ERROR) + + self._run_test(test_function) + + def test_close_on_unsupported_frame(self): + """Tests that the server sends a close frame with unsupported operation + status code when the client sends data asking some operation that is + not supported by the server. + """ + + def test_function(client): + client.connect() + + # Text frame with RSV3 bit raised. + client.send_frame_of_arbitrary_bytes('\x91\x80', '') + client.assert_receive_close( + client_for_testing.STATUS_UNSUPPORTED_DATA) + + self._run_test(test_function) + + def test_close_on_invalid_frame(self): + """Tests that the server sends a close frame with invalid frame payload + data status code when the client sends an invalid frame like containing + invalid UTF-8 character. + """ + + def test_function(client): + client.connect() + + # Text frame with invalid UTF-8 string. + client.send_message('\x80', raw=True) + client.assert_receive_close( + client_for_testing.STATUS_INVALID_FRAME_PAYLOAD_DATA) + + self._run_test(test_function) + + def test_close_on_internal_endpoint_error(self): + """Tests that the server sends a close frame with internal endpoint + error status code when the handler does bad operation. + """ + + self._options.resource = '/internal_error' + + def test_function(client): + client.connect() + client.assert_receive_close( + client_for_testing.STATUS_INTERNAL_ENDPOINT_ERROR) + + self._run_test(test_function) + + # TODO(toyoshim): Add tests to verify invalid absolute uri handling like + # host unmatch, port unmatch and invalid port description (':' without port + # number). + + def test_absolute_uri(self): + """Tests absolute uri request.""" + + options = self._options + options.resource = 'ws://localhost:%d/echo' % options.server_port + self._run_test_with_client_options(_echo_check_procedure, options) + + def test_origin_check(self): + """Tests http fallback on origin check fail.""" + + options = self._options + options.resource = '/origin_check' + # Server shows warning message for http 403 fallback. This warning + # message is confusing. Following pipe disposes warning messages. + self.server_stderr = subprocess.PIPE + self._run_http_fallback_test(options, 403) + + def test_version_check(self): + """Tests http fallback on version check fail.""" + + options = self._options + options.version = 99 + self._run_http_fallback_test(options, 400) + + +class EndToEndHyBi00Test(EndToEndTestBase): + def setUp(self): + EndToEndTestBase.setUp(self) + + def _run_test(self, test_function): + server = self._run_server() + try: + time.sleep(_SERVER_WARMUP_IN_SEC) + + client = client_for_testing.create_client_hybi00(self._options) + try: + test_function(client) + finally: + client.close_socket() + finally: + self._kill_process(server.pid) + + def test_echo(self): + self._run_test(_echo_check_procedure) + + def test_echo_server_close(self): + self._run_test(_echo_check_procedure_with_goodbye) + + +class EndToEndTestWithEchoClient(EndToEndTestBase): + def setUp(self): + EndToEndTestBase.setUp(self) + + def _check_example_echo_client_result( + self, expected, stdoutdata, stderrdata): + actual = stdoutdata.decode("utf-8") + if actual != expected: + raise Exception('Unexpected result on example echo client: ' + '%r (expected) vs %r (actual)' % + (expected, actual)) + if stderrdata is not None: + raise Exception('Unexpected error message on example echo ' + 'client: %r' % stderrdata) + + def test_example_echo_client(self): + """Tests that the echo_client.py example can talk with the server.""" + + server = self._run_server() + try: + time.sleep(_SERVER_WARMUP_IN_SEC) + + client_command = os.path.join( + self.top_dir, 'example', 'echo_client.py') + + # Expected output for the default messages. + default_expectation = ('Send: Hello\n' 'Recv: Hello\n' + u'Send: \u65e5\u672c\n' u'Recv: \u65e5\u672c\n' + 'Send close\n' 'Recv ack\n') + + args = [client_command, + '-p', str(self._options.server_port)] + client = self._run_python_command(args, stdout=subprocess.PIPE) + stdoutdata, stderrdata = client.communicate() + self._check_example_echo_client_result( + default_expectation, stdoutdata, stderrdata) + + # Process a big message for which extended payload length is used. + # To handle extended payload length, ws_version attribute will be + # accessed. This test checks that ws_version is correctly set. + big_message = 'a' * 1024 + args = [client_command, + '-p', str(self._options.server_port), + '-m', big_message] + client = self._run_python_command(args, stdout=subprocess.PIPE) + stdoutdata, stderrdata = client.communicate() + expected = ('Send: %s\nRecv: %s\nSend close\nRecv ack\n' % + (big_message, big_message)) + self._check_example_echo_client_result( + expected, stdoutdata, stderrdata) + + # Test the permessage-deflate extension. + args = [client_command, + '-p', str(self._options.server_port), + '--use_permessage_deflate'] + client = self._run_python_command(args, stdout=subprocess.PIPE) + stdoutdata, stderrdata = client.communicate() + self._check_example_echo_client_result( + default_expectation, stdoutdata, stderrdata) + finally: + self._kill_process(server.pid) + + +if __name__ == '__main__': + unittest.main() + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_extensions.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_extensions.py new file mode 100755 index 000000000..6c8b1262d --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_extensions.py @@ -0,0 +1,360 @@ +#!/usr/bin/env python +# +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Tests for extensions module.""" + + +import unittest +import zlib + +import set_sys_path # Update sys.path to locate mod_pywebsocket module. + +from mod_pywebsocket import common +from mod_pywebsocket import extensions + + +class ExtensionsTest(unittest.TestCase): + """A unittest for non-class methods in extensions.py""" + + def test_parse_window_bits(self): + self.assertRaises(ValueError, extensions._parse_window_bits, None) + self.assertRaises(ValueError, extensions._parse_window_bits, 'foobar') + self.assertRaises(ValueError, extensions._parse_window_bits, ' 8 ') + self.assertRaises(ValueError, extensions._parse_window_bits, 'a8a') + self.assertRaises(ValueError, extensions._parse_window_bits, '00000') + self.assertRaises(ValueError, extensions._parse_window_bits, '00008') + self.assertRaises(ValueError, extensions._parse_window_bits, '0x8') + + self.assertRaises(ValueError, extensions._parse_window_bits, '9.5') + self.assertRaises(ValueError, extensions._parse_window_bits, '8.0') + + self.assertTrue(extensions._parse_window_bits, '8') + self.assertTrue(extensions._parse_window_bits, '15') + + self.assertRaises(ValueError, extensions._parse_window_bits, '-8') + self.assertRaises(ValueError, extensions._parse_window_bits, '0') + self.assertRaises(ValueError, extensions._parse_window_bits, '7') + + self.assertRaises(ValueError, extensions._parse_window_bits, '16') + self.assertRaises( + ValueError, extensions._parse_window_bits, '10000000') + + +class CompressionMethodParameterParserTest(unittest.TestCase): + """A unittest for _parse_compression_method which parses the compression + method description used by perframe-compression and permessage-compression + extension in their "method" extension parameter. + """ + + def test_parse_method_simple(self): + method_list = extensions._parse_compression_method('foo') + self.assertEqual(1, len(method_list)) + method = method_list[0] + self.assertEqual('foo', method.name()) + self.assertEqual(0, len(method.get_parameters())) + + def test_parse_method_with_parameter(self): + method_list = extensions._parse_compression_method('foo; x; y=10') + self.assertEqual(1, len(method_list)) + method = method_list[0] + self.assertEqual('foo', method.name()) + self.assertEqual(2, len(method.get_parameters())) + self.assertTrue(method.has_parameter('x')) + self.assertEqual(None, method.get_parameter_value('x')) + self.assertTrue(method.has_parameter('y')) + self.assertEqual('10', method.get_parameter_value('y')) + + def test_parse_method_with_quoted_parameter(self): + method_list = extensions._parse_compression_method( + 'foo; x="Hello World"; y=10') + self.assertEqual(1, len(method_list)) + method = method_list[0] + self.assertEqual('foo', method.name()) + self.assertEqual(2, len(method.get_parameters())) + self.assertTrue(method.has_parameter('x')) + self.assertEqual('Hello World', method.get_parameter_value('x')) + self.assertTrue(method.has_parameter('y')) + self.assertEqual('10', method.get_parameter_value('y')) + + def test_parse_method_multiple(self): + method_list = extensions._parse_compression_method('foo, bar') + self.assertEqual(2, len(method_list)) + self.assertEqual('foo', method_list[0].name()) + self.assertEqual(0, len(method_list[0].get_parameters())) + self.assertEqual('bar', method_list[1].name()) + self.assertEqual(0, len(method_list[1].get_parameters())) + + def test_parse_method_multiple_methods_with_quoted_parameter(self): + method_list = extensions._parse_compression_method( + 'foo; x="Hello World", bar; y=10') + self.assertEqual(2, len(method_list)) + self.assertEqual('foo', method_list[0].name()) + self.assertEqual(1, len(method_list[0].get_parameters())) + self.assertTrue(method_list[0].has_parameter('x')) + self.assertEqual('Hello World', + method_list[0].get_parameter_value('x')) + self.assertEqual('bar', method_list[1].name()) + self.assertEqual(1, len(method_list[1].get_parameters())) + self.assertTrue(method_list[1].has_parameter('y')) + self.assertEqual('10', method_list[1].get_parameter_value('y')) + + def test_create_method_desc_simple(self): + params = common.ExtensionParameter('foo') + desc = extensions._create_accepted_method_desc('foo', + params.get_parameters()) + self.assertEqual('foo', desc) + + def test_create_method_desc_with_parameters(self): + params = common.ExtensionParameter('foo') + params.add_parameter('x', 'Hello, World') + params.add_parameter('y', '10') + desc = extensions._create_accepted_method_desc('foo', + params.get_parameters()) + self.assertEqual('foo; x="Hello, World"; y=10', desc) + + +class DeflateFrameExtensionProcessorParsingTest(unittest.TestCase): + """A unittest for checking that DeflateFrameExtensionProcessor parses given + extension parameter correctly. + """ + + def test_registry(self): + processor = extensions.get_extension_processor( + common.ExtensionParameter('deflate-frame')) + self.assertIsInstance(processor, + extensions.DeflateFrameExtensionProcessor) + + processor = extensions.get_extension_processor( + common.ExtensionParameter('x-webkit-deflate-frame')) + self.assertIsInstance(processor, + extensions.DeflateFrameExtensionProcessor) + + def test_minimal_offer(self): + processor = extensions.DeflateFrameExtensionProcessor( + common.ExtensionParameter('perframe-deflate')) + + response = processor.get_extension_response() + self.assertEqual('perframe-deflate', response.name()) + self.assertEqual(0, len(response.get_parameters())) + + self.assertEqual(zlib.MAX_WBITS, + processor._rfc1979_deflater._window_bits) + self.assertFalse(processor._rfc1979_deflater._no_context_takeover) + + def test_offer_with_max_window_bits(self): + parameter = common.ExtensionParameter('perframe-deflate') + parameter.add_parameter('max_window_bits', '10') + processor = extensions.DeflateFrameExtensionProcessor(parameter) + + response = processor.get_extension_response() + self.assertEqual('perframe-deflate', response.name()) + self.assertEqual(0, len(response.get_parameters())) + + self.assertEqual(10, processor._rfc1979_deflater._window_bits) + + def test_offer_with_out_of_range_max_window_bits(self): + parameter = common.ExtensionParameter('perframe-deflate') + parameter.add_parameter('max_window_bits', '0') + processor = extensions.DeflateFrameExtensionProcessor(parameter) + + self.assertIsNone(processor.get_extension_response()) + + def test_offer_with_max_window_bits_without_value(self): + parameter = common.ExtensionParameter('perframe-deflate') + parameter.add_parameter('max_window_bits', None) + processor = extensions.DeflateFrameExtensionProcessor(parameter) + + self.assertIsNone(processor.get_extension_response()) + + def test_offer_with_no_context_takeover(self): + parameter = common.ExtensionParameter('perframe-deflate') + parameter.add_parameter('no_context_takeover', None) + processor = extensions.DeflateFrameExtensionProcessor(parameter) + + response = processor.get_extension_response() + self.assertEqual('perframe-deflate', response.name()) + self.assertEqual(0, len(response.get_parameters())) + + self.assertTrue(processor._rfc1979_deflater._no_context_takeover) + + def test_offer_with_no_context_takeover_with_value(self): + parameter = common.ExtensionParameter('perframe-deflate') + parameter.add_parameter('no_context_takeover', 'foobar') + processor = extensions.DeflateFrameExtensionProcessor(parameter) + + self.assertIsNone(processor.get_extension_response()) + + def test_offer_with_unknown_parameter(self): + parameter = common.ExtensionParameter('perframe-deflate') + parameter.add_parameter('foo', 'bar') + processor = extensions.DeflateFrameExtensionProcessor(parameter) + + response = processor.get_extension_response() + self.assertEqual('perframe-deflate', response.name()) + self.assertEqual(0, len(response.get_parameters())) + + +class PerMessageDeflateExtensionProcessorParsingTest(unittest.TestCase): + """A unittest for checking that PerMessageDeflateExtensionProcessor parses + given extension parameter correctly. + """ + + def test_registry(self): + processor = extensions.get_extension_processor( + common.ExtensionParameter('permessage-deflate')) + self.assertIsInstance(processor, + extensions.PerMessageDeflateExtensionProcessor) + + def test_minimal_offer(self): + processor = extensions.PerMessageDeflateExtensionProcessor( + common.ExtensionParameter('permessage-deflate')) + + response = processor.get_extension_response() + self.assertEqual('permessage-deflate', response.name()) + self.assertEqual(0, len(response.get_parameters())) + + self.assertEqual(zlib.MAX_WBITS, + processor._rfc1979_deflater._window_bits) + self.assertFalse(processor._rfc1979_deflater._no_context_takeover) + + def test_offer_with_max_window_bits(self): + parameter = common.ExtensionParameter('permessage-deflate') + parameter.add_parameter('server_max_window_bits', '10') + processor = extensions.PerMessageDeflateExtensionProcessor(parameter) + + response = processor.get_extension_response() + self.assertEqual('permessage-deflate', response.name()) + self.assertEqual([('server_max_window_bits', '10')], + response.get_parameters()) + + self.assertEqual(10, processor._rfc1979_deflater._window_bits) + + def test_offer_with_out_of_range_max_window_bits(self): + parameter = common.ExtensionParameter('permessage-deflate') + parameter.add_parameter('server_max_window_bits', '0') + processor = extensions.PerMessageDeflateExtensionProcessor(parameter) + + self.assertIsNone(processor.get_extension_response()) + + def test_offer_with_max_window_bits_without_value(self): + parameter = common.ExtensionParameter('permessage-deflate') + parameter.add_parameter('server_max_window_bits', None) + processor = extensions.PerMessageDeflateExtensionProcessor(parameter) + + self.assertIsNone(processor.get_extension_response()) + + def test_offer_with_no_context_takeover(self): + parameter = common.ExtensionParameter('permessage-deflate') + parameter.add_parameter('server_no_context_takeover', None) + processor = extensions.PerMessageDeflateExtensionProcessor(parameter) + + response = processor.get_extension_response() + self.assertEqual('permessage-deflate', response.name()) + self.assertEqual([('server_no_context_takeover', None)], + response.get_parameters()) + + self.assertTrue(processor._rfc1979_deflater._no_context_takeover) + + def test_offer_with_no_context_takeover_with_value(self): + parameter = common.ExtensionParameter('permessage-deflate') + parameter.add_parameter('server_no_context_takeover', 'foobar') + processor = extensions.PerMessageDeflateExtensionProcessor(parameter) + + self.assertIsNone(processor.get_extension_response()) + + def test_offer_with_unknown_parameter(self): + parameter = common.ExtensionParameter('permessage-deflate') + parameter.add_parameter('foo', 'bar') + processor = extensions.PerMessageDeflateExtensionProcessor(parameter) + + self.assertIsNone(processor.get_extension_response()) + + +class PerMessageDeflateExtensionProcessorBuildingTest(unittest.TestCase): + """A unittest for checking that PerMessageDeflateExtensionProcessor builds + a response based on specified options correctly. + """ + + def test_response_with_max_window_bits(self): + parameter = common.ExtensionParameter('permessage-deflate') + parameter.add_parameter('client_max_window_bits', None) + processor = extensions.PerMessageDeflateExtensionProcessor(parameter) + processor.set_client_max_window_bits(10) + + response = processor.get_extension_response() + self.assertEqual('permessage-deflate', response.name()) + self.assertEqual([('client_max_window_bits', '10')], + response.get_parameters()) + + def test_response_with_max_window_bits_without_client_permission(self): + processor = extensions.PerMessageDeflateExtensionProcessor( + common.ExtensionParameter('permessage-deflate')) + processor.set_client_max_window_bits(10) + + response = processor.get_extension_response() + self.assertIsNone(response) + + def test_response_with_true_for_no_context_takeover(self): + processor = extensions.PerMessageDeflateExtensionProcessor( + common.ExtensionParameter('permessage-deflate')) + + processor.set_client_no_context_takeover(True) + + response = processor.get_extension_response() + self.assertEqual('permessage-deflate', response.name()) + self.assertEqual([('client_no_context_takeover', None)], + response.get_parameters()) + + def test_response_with_false_for_no_context_takeover(self): + processor = extensions.PerMessageDeflateExtensionProcessor( + common.ExtensionParameter('permessage-deflate')) + + processor.set_client_no_context_takeover(False) + + response = processor.get_extension_response() + self.assertEqual('permessage-deflate', response.name()) + self.assertEqual(0, len(response.get_parameters())) + + +class PerMessageCompressExtensionProcessorTest(unittest.TestCase): + def test_registry(self): + processor = extensions.get_extension_processor( + common.ExtensionParameter('permessage-compress')) + self.assertIsInstance(processor, + extensions.PerMessageCompressExtensionProcessor) + + +if __name__ == '__main__': + unittest.main() + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake.py new file mode 100755 index 000000000..aa78ac05e --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python +# +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Tests for handshake._base module.""" + + +import unittest + +import set_sys_path # Update sys.path to locate mod_pywebsocket module. + +from mod_pywebsocket.common import ExtensionParameter +from mod_pywebsocket.common import ExtensionParsingException +from mod_pywebsocket.common import format_extensions +from mod_pywebsocket.common import parse_extensions +from mod_pywebsocket.handshake._base import HandshakeException +from mod_pywebsocket.handshake._base import validate_subprotocol + + +class ValidateSubprotocolTest(unittest.TestCase): + """A unittest for validate_subprotocol method.""" + + def test_validate_subprotocol(self): + # Should succeed. + validate_subprotocol('sample') + validate_subprotocol('Sample') + validate_subprotocol('sample\x7eprotocol') + + # Should fail. + self.assertRaises(HandshakeException, + validate_subprotocol, + '') + self.assertRaises(HandshakeException, + validate_subprotocol, + 'sample\x09protocol') + self.assertRaises(HandshakeException, + validate_subprotocol, + 'sample\x19protocol') + self.assertRaises(HandshakeException, + validate_subprotocol, + 'sample\x20protocol') + self.assertRaises(HandshakeException, + validate_subprotocol, + 'sample\x7fprotocol') + self.assertRaises(HandshakeException, + validate_subprotocol, + # "Japan" in Japanese + u'\u65e5\u672c') + + +_TEST_TOKEN_EXTENSION_DATA = [ + ('foo', [('foo', [])]), + ('foo; bar', [('foo', [('bar', None)])]), + ('foo; bar=baz', [('foo', [('bar', 'baz')])]), + ('foo; bar=baz; car=cdr', [('foo', [('bar', 'baz'), ('car', 'cdr')])]), + ('foo; bar=baz, car; cdr', + [('foo', [('bar', 'baz')]), ('car', [('cdr', None)])]), + ('a, b, c, d', + [('a', []), ('b', []), ('c', []), ('d', [])]), + ] + + +_TEST_QUOTED_EXTENSION_DATA = [ + ('foo; bar=""', [('foo', [('bar', '')])]), + ('foo; bar=" baz "', [('foo', [('bar', ' baz ')])]), + ('foo; bar=",baz;"', [('foo', [('bar', ',baz;')])]), + ('foo; bar="\\\r\\\nbaz"', [('foo', [('bar', '\r\nbaz')])]), + ('foo; bar="\\"baz"', [('foo', [('bar', '"baz')])]), + ('foo; bar="\xbbbaz"', [('foo', [('bar', '\xbbbaz')])]), + ] + + +_TEST_REDUNDANT_TOKEN_EXTENSION_DATA = [ + ('foo \t ', [('foo', [])]), + ('foo; \r\n bar', [('foo', [('bar', None)])]), + ('foo; bar=\r\n \r\n baz', [('foo', [('bar', 'baz')])]), + ('foo ;bar = baz ', [('foo', [('bar', 'baz')])]), + ('foo,bar,,baz', [('foo', []), ('bar', []), ('baz', [])]), + ] + + +_TEST_REDUNDANT_QUOTED_EXTENSION_DATA = [ + ('foo; bar="\r\n \r\n baz"', [('foo', [('bar', ' baz')])]), + ] + + +class ExtensionsParserTest(unittest.TestCase): + + def _verify_extension_list(self, expected_list, actual_list): + """Verifies that ExtensionParameter objects in actual_list have the + same members as extension definitions in expected_list. Extension + definition used in this test is a pair of an extension name and a + parameter dictionary. + """ + + self.assertEqual(len(expected_list), len(actual_list)) + for expected, actual in zip(expected_list, actual_list): + (name, parameters) = expected + self.assertEqual(name, actual._name) + self.assertEqual(parameters, actual._parameters) + + def test_parse(self): + for formatted_string, definition in _TEST_TOKEN_EXTENSION_DATA: + self._verify_extension_list( + definition, parse_extensions(formatted_string)) + + def test_parse_quoted_data(self): + for formatted_string, definition in _TEST_QUOTED_EXTENSION_DATA: + self._verify_extension_list( + definition, parse_extensions(formatted_string)) + + def test_parse_redundant_data(self): + for (formatted_string, + definition) in _TEST_REDUNDANT_TOKEN_EXTENSION_DATA: + self._verify_extension_list( + definition, parse_extensions(formatted_string)) + + def test_parse_redundant_quoted_data(self): + for (formatted_string, + definition) in _TEST_REDUNDANT_QUOTED_EXTENSION_DATA: + self._verify_extension_list( + definition, parse_extensions(formatted_string)) + + def test_parse_bad_data(self): + _TEST_BAD_EXTENSION_DATA = [ + ('foo; ; '), + ('foo; a a'), + ('foo foo'), + (',,,'), + ('foo; bar='), + ('foo; bar="hoge'), + ('foo; bar="a\r"'), + ('foo; bar="\\\xff"'), + ('foo; bar=\ra'), + ] + + for formatted_string in _TEST_BAD_EXTENSION_DATA: + self.assertRaises( + ExtensionParsingException, parse_extensions, formatted_string) + + +class FormatExtensionsTest(unittest.TestCase): + + def test_format_extensions(self): + for formatted_string, definitions in _TEST_TOKEN_EXTENSION_DATA: + extensions = [] + for definition in definitions: + (name, parameters) = definition + extension = ExtensionParameter(name) + extension._parameters = parameters + extensions.append(extension) + self.assertEqual( + formatted_string, format_extensions(extensions)) + + +if __name__ == '__main__': + unittest.main() + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi.py new file mode 100755 index 000000000..6c8713823 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi.py @@ -0,0 +1,534 @@ +#!/usr/bin/env python +# +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Tests for handshake module.""" + + +import unittest + +import set_sys_path # Update sys.path to locate mod_pywebsocket module. +from mod_pywebsocket import common +from mod_pywebsocket.handshake._base import AbortedByUserException +from mod_pywebsocket.handshake._base import HandshakeException +from mod_pywebsocket.handshake._base import VersionException +from mod_pywebsocket.handshake.hybi import Handshaker + +import mock + + +class RequestDefinition(object): + """A class for holding data for constructing opening handshake strings for + testing the opening handshake processor. + """ + + def __init__(self, method, uri, headers): + self.method = method + self.uri = uri + self.headers = headers + + +def _create_good_request_def(): + return RequestDefinition( + 'GET', '/demo', + {'Host': 'server.example.com', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': '13', + 'Origin': 'http://example.com'}) + + +def _create_request(request_def): + conn = mock.MockConn('') + return mock.MockRequest( + method=request_def.method, + uri=request_def.uri, + headers_in=request_def.headers, + connection=conn) + + +def _create_handshaker(request): + handshaker = Handshaker(request, mock.MockDispatcher()) + return handshaker + + +class SubprotocolChoosingDispatcher(object): + """A dispatcher for testing. This dispatcher sets the i-th subprotocol + of requested ones to ws_protocol where i is given on construction as index + argument. If index is negative, default_value will be set to ws_protocol. + """ + + def __init__(self, index, default_value=None): + self.index = index + self.default_value = default_value + + def do_extra_handshake(self, conn_context): + if self.index >= 0: + conn_context.ws_protocol = conn_context.ws_requested_protocols[ + self.index] + else: + conn_context.ws_protocol = self.default_value + + def transfer_data(self, conn_context): + pass + + +class HandshakeAbortedException(Exception): + pass + + +class AbortingDispatcher(object): + """A dispatcher for testing. This dispatcher raises an exception in + do_extra_handshake to reject the request. + """ + + def do_extra_handshake(self, conn_context): + raise HandshakeAbortedException('An exception to reject the request') + + def transfer_data(self, conn_context): + pass + + +class AbortedByUserDispatcher(object): + """A dispatcher for testing. This dispatcher raises an + AbortedByUserException in do_extra_handshake to reject the request. + """ + + def do_extra_handshake(self, conn_context): + raise AbortedByUserException('An AbortedByUserException to reject the ' + 'request') + + def transfer_data(self, conn_context): + pass + + +_EXPECTED_RESPONSE = ( + 'HTTP/1.1 101 Switching Protocols\r\n' + 'Upgrade: websocket\r\n' + 'Connection: Upgrade\r\n' + 'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n') + + +class HandshakerTest(unittest.TestCase): + """A unittest for draft-ietf-hybi-thewebsocketprotocol-06 and later + handshake processor. + """ + + def test_do_handshake(self): + request = _create_request(_create_good_request_def()) + dispatcher = mock.MockDispatcher() + handshaker = Handshaker(request, dispatcher) + handshaker.do_handshake() + + self.assertTrue(dispatcher.do_extra_handshake_called) + + self.assertEqual( + _EXPECTED_RESPONSE, request.connection.written_data()) + self.assertEqual('/demo', request.ws_resource) + self.assertEqual('http://example.com', request.ws_origin) + self.assertEqual(None, request.ws_protocol) + self.assertEqual(None, request.ws_extensions) + self.assertEqual(common.VERSION_HYBI_LATEST, request.ws_version) + + def test_do_handshake_with_extra_headers(self): + request_def = _create_good_request_def() + # Add headers not related to WebSocket opening handshake. + request_def.headers['FooKey'] = 'BarValue' + request_def.headers['EmptyKey'] = '' + + request = _create_request(request_def) + handshaker = _create_handshaker(request) + handshaker.do_handshake() + self.assertEqual( + _EXPECTED_RESPONSE, request.connection.written_data()) + + def test_do_handshake_with_capitalized_value(self): + request_def = _create_good_request_def() + request_def.headers['upgrade'] = 'WEBSOCKET' + + request = _create_request(request_def) + handshaker = _create_handshaker(request) + handshaker.do_handshake() + self.assertEqual( + _EXPECTED_RESPONSE, request.connection.written_data()) + + request_def = _create_good_request_def() + request_def.headers['Connection'] = 'UPGRADE' + + request = _create_request(request_def) + handshaker = _create_handshaker(request) + handshaker.do_handshake() + self.assertEqual( + _EXPECTED_RESPONSE, request.connection.written_data()) + + def test_do_handshake_with_multiple_connection_values(self): + request_def = _create_good_request_def() + request_def.headers['Connection'] = 'Upgrade, keep-alive, , ' + + request = _create_request(request_def) + handshaker = _create_handshaker(request) + handshaker.do_handshake() + self.assertEqual( + _EXPECTED_RESPONSE, request.connection.written_data()) + + def test_aborting_handshake(self): + handshaker = Handshaker( + _create_request(_create_good_request_def()), + AbortingDispatcher()) + # do_extra_handshake raises an exception. Check that it's not caught by + # do_handshake. + self.assertRaises(HandshakeAbortedException, handshaker.do_handshake) + + def test_do_handshake_with_protocol(self): + request_def = _create_good_request_def() + request_def.headers['Sec-WebSocket-Protocol'] = 'chat, superchat' + + request = _create_request(request_def) + handshaker = Handshaker(request, SubprotocolChoosingDispatcher(0)) + handshaker.do_handshake() + + EXPECTED_RESPONSE = ( + 'HTTP/1.1 101 Switching Protocols\r\n' + 'Upgrade: websocket\r\n' + 'Connection: Upgrade\r\n' + 'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n' + 'Sec-WebSocket-Protocol: chat\r\n\r\n') + + self.assertEqual(EXPECTED_RESPONSE, request.connection.written_data()) + self.assertEqual('chat', request.ws_protocol) + + def test_do_handshake_protocol_not_in_request_but_in_response(self): + request_def = _create_good_request_def() + request = _create_request(request_def) + handshaker = Handshaker( + request, SubprotocolChoosingDispatcher(-1, 'foobar')) + # No request has been made but ws_protocol is set. HandshakeException + # must be raised. + self.assertRaises(HandshakeException, handshaker.do_handshake) + + def test_do_handshake_with_protocol_no_protocol_selection(self): + request_def = _create_good_request_def() + request_def.headers['Sec-WebSocket-Protocol'] = 'chat, superchat' + + request = _create_request(request_def) + handshaker = _create_handshaker(request) + # ws_protocol is not set. HandshakeException must be raised. + self.assertRaises(HandshakeException, handshaker.do_handshake) + + def test_do_handshake_with_extensions(self): + request_def = _create_good_request_def() + request_def.headers['Sec-WebSocket-Extensions'] = ( + 'permessage-compress; method=deflate, unknown') + + EXPECTED_RESPONSE = ( + 'HTTP/1.1 101 Switching Protocols\r\n' + 'Upgrade: websocket\r\n' + 'Connection: Upgrade\r\n' + 'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n' + 'Sec-WebSocket-Extensions: permessage-compress; method=deflate\r\n' + '\r\n') + + request = _create_request(request_def) + handshaker = _create_handshaker(request) + handshaker.do_handshake() + self.assertEqual(EXPECTED_RESPONSE, request.connection.written_data()) + self.assertEqual(1, len(request.ws_extensions)) + extension = request.ws_extensions[0] + self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION, + extension.name()) + self.assertEqual(['method'], extension.get_parameter_names()) + self.assertEqual('deflate', extension.get_parameter_value('method')) + self.assertEqual(1, len(request.ws_extension_processors)) + self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION, + request.ws_extension_processors[0].name()) + + def test_do_handshake_with_permessage_compress(self): + request_def = _create_good_request_def() + request_def.headers['Sec-WebSocket-Extensions'] = ( + 'permessage-compress; method=deflate') + request = _create_request(request_def) + handshaker = _create_handshaker(request) + handshaker.do_handshake() + self.assertEqual(1, len(request.ws_extensions)) + self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION, + request.ws_extensions[0].name()) + self.assertEqual(1, len(request.ws_extension_processors)) + self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION, + request.ws_extension_processors[0].name()) + + def test_do_handshake_with_quoted_extensions(self): + request_def = _create_good_request_def() + request_def.headers['Sec-WebSocket-Extensions'] = ( + 'permessage-compress; method=deflate, , ' + 'unknown; e = "mc^2"; ma="\r\n \\\rf "; pv=nrt') + + request = _create_request(request_def) + handshaker = _create_handshaker(request) + handshaker.do_handshake() + self.assertEqual(2, len(request.ws_requested_extensions)) + first_extension = request.ws_requested_extensions[0] + self.assertEqual('permessage-compress', first_extension.name()) + self.assertEqual(['method'], first_extension.get_parameter_names()) + self.assertEqual('deflate', + first_extension.get_parameter_value('method')) + second_extension = request.ws_requested_extensions[1] + self.assertEqual('unknown', second_extension.name()) + self.assertEqual( + ['e', 'ma', 'pv'], second_extension.get_parameter_names()) + self.assertEqual('mc^2', second_extension.get_parameter_value('e')) + self.assertEqual(' \rf ', second_extension.get_parameter_value('ma')) + self.assertEqual('nrt', second_extension.get_parameter_value('pv')) + + def test_do_handshake_with_optional_headers(self): + request_def = _create_good_request_def() + request_def.headers['EmptyValue'] = '' + request_def.headers['AKey'] = 'AValue' + + request = _create_request(request_def) + handshaker = _create_handshaker(request) + handshaker.do_handshake() + self.assertEqual( + 'AValue', request.headers_in['AKey']) + self.assertEqual( + '', request.headers_in['EmptyValue']) + + def test_abort_extra_handshake(self): + handshaker = Handshaker( + _create_request(_create_good_request_def()), + AbortedByUserDispatcher()) + # do_extra_handshake raises an AbortedByUserException. Check that it's + # not caught by do_handshake. + self.assertRaises(AbortedByUserException, handshaker.do_handshake) + + def test_do_handshake_with_mux_and_deflate_frame(self): + request_def = _create_good_request_def() + request_def.headers['Sec-WebSocket-Extensions'] = ('%s, %s' % ( + common.MUX_EXTENSION, + common.DEFLATE_FRAME_EXTENSION)) + request = _create_request(request_def) + handshaker = _create_handshaker(request) + handshaker.do_handshake() + # mux should be rejected. + self.assertEqual(1, len(request.ws_extensions)) + self.assertEqual(common.DEFLATE_FRAME_EXTENSION, + request.ws_extensions[0].name()) + self.assertEqual(2, len(request.ws_extension_processors)) + self.assertEqual(common.MUX_EXTENSION, + request.ws_extension_processors[0].name()) + self.assertEqual(common.DEFLATE_FRAME_EXTENSION, + request.ws_extension_processors[1].name()) + self.assertFalse(hasattr(request, 'mux_processor')) + + def test_do_handshake_with_deflate_frame_and_mux(self): + request_def = _create_good_request_def() + request_def.headers['Sec-WebSocket-Extensions'] = ('%s, %s' % ( + common.DEFLATE_FRAME_EXTENSION, + common.MUX_EXTENSION)) + request = _create_request(request_def) + handshaker = _create_handshaker(request) + handshaker.do_handshake() + # mux should be rejected. + self.assertEqual(1, len(request.ws_extensions)) + first_extension = request.ws_extensions[0] + self.assertEqual(common.DEFLATE_FRAME_EXTENSION, + first_extension.name()) + self.assertEqual(2, len(request.ws_extension_processors)) + self.assertEqual(common.DEFLATE_FRAME_EXTENSION, + request.ws_extension_processors[0].name()) + self.assertEqual(common.MUX_EXTENSION, + request.ws_extension_processors[1].name()) + self.assertFalse(hasattr(request, 'mux')) + + def test_do_handshake_with_permessage_compress_and_mux(self): + request_def = _create_good_request_def() + request_def.headers['Sec-WebSocket-Extensions'] = ( + '%s; method=deflate, %s' % ( + common.PERMESSAGE_COMPRESSION_EXTENSION, + common.MUX_EXTENSION)) + request = _create_request(request_def) + handshaker = _create_handshaker(request) + handshaker.do_handshake() + + self.assertEqual(1, len(request.ws_extensions)) + self.assertEqual(common.MUX_EXTENSION, + request.ws_extensions[0].name()) + self.assertEqual(2, len(request.ws_extension_processors)) + self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION, + request.ws_extension_processors[0].name()) + self.assertEqual(common.MUX_EXTENSION, + request.ws_extension_processors[1].name()) + self.assertTrue(hasattr(request, 'mux_processor')) + self.assertTrue(request.mux_processor.is_active()) + mux_extensions = request.mux_processor.extensions() + self.assertEqual(1, len(mux_extensions)) + self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION, + mux_extensions[0].name()) + + def test_do_handshake_with_mux_and_permessage_compress(self): + request_def = _create_good_request_def() + request_def.headers['Sec-WebSocket-Extensions'] = ( + '%s, %s; method=deflate' % ( + common.MUX_EXTENSION, + common.PERMESSAGE_COMPRESSION_EXTENSION)) + request = _create_request(request_def) + handshaker = _create_handshaker(request) + handshaker.do_handshake() + # mux should be rejected. + self.assertEqual(1, len(request.ws_extensions)) + first_extension = request.ws_extensions[0] + self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION, + first_extension.name()) + self.assertEqual(2, len(request.ws_extension_processors)) + self.assertEqual(common.MUX_EXTENSION, + request.ws_extension_processors[0].name()) + self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION, + request.ws_extension_processors[1].name()) + self.assertFalse(hasattr(request, 'mux_processor')) + + def test_bad_requests(self): + bad_cases = [ + ('HTTP request', + RequestDefinition( + 'GET', '/demo', + {'Host': 'www.google.com', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5;' + ' en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3' + ' GTB6 GTBA', + 'Accept': + 'text/html,application/xhtml+xml,application/xml;q=0.9,' + '*/*;q=0.8', + 'Accept-Language': 'en-us,en;q=0.5', + 'Accept-Encoding': 'gzip,deflate', + 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', + 'Keep-Alive': '300', + 'Connection': 'keep-alive'}), None, True)] + + request_def = _create_good_request_def() + request_def.method = 'POST' + bad_cases.append(('Wrong method', request_def, None, True)) + + request_def = _create_good_request_def() + del request_def.headers['Host'] + bad_cases.append(('Missing Host', request_def, None, True)) + + request_def = _create_good_request_def() + del request_def.headers['Upgrade'] + bad_cases.append(('Missing Upgrade', request_def, None, True)) + + request_def = _create_good_request_def() + request_def.headers['Upgrade'] = 'nonwebsocket' + bad_cases.append(('Wrong Upgrade', request_def, None, True)) + + request_def = _create_good_request_def() + del request_def.headers['Connection'] + bad_cases.append(('Missing Connection', request_def, None, True)) + + request_def = _create_good_request_def() + request_def.headers['Connection'] = 'Downgrade' + bad_cases.append(('Wrong Connection', request_def, None, True)) + + request_def = _create_good_request_def() + del request_def.headers['Sec-WebSocket-Key'] + bad_cases.append(('Missing Sec-WebSocket-Key', request_def, 400, True)) + + request_def = _create_good_request_def() + request_def.headers['Sec-WebSocket-Key'] = ( + 'dGhlIHNhbXBsZSBub25jZQ==garbage') + bad_cases.append(('Wrong Sec-WebSocket-Key (with garbage on the tail)', + request_def, 400, True)) + + request_def = _create_good_request_def() + request_def.headers['Sec-WebSocket-Key'] = 'YQ==' # BASE64 of 'a' + bad_cases.append( + ('Wrong Sec-WebSocket-Key (decoded value is not 16 octets long)', + request_def, 400, True)) + + request_def = _create_good_request_def() + # The last character right before == must be any of A, Q, w and g. + request_def.headers['Sec-WebSocket-Key'] = ( + 'AQIDBAUGBwgJCgsMDQ4PEC==') + bad_cases.append( + ('Wrong Sec-WebSocket-Key (padding bits are not zero)', + request_def, 400, True)) + + request_def = _create_good_request_def() + request_def.headers['Sec-WebSocket-Key'] = ( + 'dGhlIHNhbXBsZSBub25jZQ==,dGhlIHNhbXBsZSBub25jZQ==') + bad_cases.append( + ('Wrong Sec-WebSocket-Key (multiple values)', + request_def, 400, True)) + + request_def = _create_good_request_def() + del request_def.headers['Sec-WebSocket-Version'] + bad_cases.append(('Missing Sec-WebSocket-Version', request_def, None, + True)) + + request_def = _create_good_request_def() + request_def.headers['Sec-WebSocket-Version'] = '3' + bad_cases.append(('Wrong Sec-WebSocket-Version', request_def, None, + False)) + + request_def = _create_good_request_def() + request_def.headers['Sec-WebSocket-Version'] = '13, 13' + bad_cases.append(('Wrong Sec-WebSocket-Version (multiple values)', + request_def, 400, True)) + + request_def = _create_good_request_def() + request_def.headers['Sec-WebSocket-Protocol'] = 'illegal\x09protocol' + bad_cases.append(('Illegal Sec-WebSocket-Protocol', + request_def, 400, True)) + + request_def = _create_good_request_def() + request_def.headers['Sec-WebSocket-Protocol'] = '' + bad_cases.append(('Empty Sec-WebSocket-Protocol', + request_def, 400, True)) + + for (case_name, request_def, expected_status, + expect_handshake_exception) in bad_cases: + request = _create_request(request_def) + handshaker = Handshaker(request, mock.MockDispatcher()) + try: + handshaker.do_handshake() + self.fail('No exception thrown for \'%s\' case' % case_name) + except HandshakeException, e: + self.assertTrue(expect_handshake_exception) + self.assertEqual(expected_status, e.status) + except VersionException, e: + self.assertFalse(expect_handshake_exception) + + +if __name__ == '__main__': + unittest.main() + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi00.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi00.py new file mode 100755 index 000000000..73f9f27ca --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi00.py @@ -0,0 +1,516 @@ +#!/usr/bin/env python +# +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Tests for handshake.hybi00 module.""" + + +import unittest + +import set_sys_path # Update sys.path to locate mod_pywebsocket module. + +from mod_pywebsocket.handshake._base import HandshakeException +from mod_pywebsocket.handshake.hybi00 import Handshaker +from mod_pywebsocket.handshake.hybi00 import _validate_subprotocol +from test import mock + + +_TEST_KEY1 = '4 @1 46546xW%0l 1 5' +_TEST_KEY2 = '12998 5 Y3 1 .P00' +_TEST_KEY3 = '^n:ds[4U' +_TEST_CHALLENGE_RESPONSE = '8jKS\'y:G*Co,Wxa-' + + +_GOOD_REQUEST = ( + 80, + 'GET', + '/demo', + { + 'Host': 'example.com', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key2': _TEST_KEY2, + 'Sec-WebSocket-Protocol': 'sample', + 'Upgrade': 'WebSocket', + 'Sec-WebSocket-Key1': _TEST_KEY1, + 'Origin': 'http://example.com', + }, + _TEST_KEY3) + +_GOOD_REQUEST_CAPITALIZED_HEADER_VALUES = ( + 80, + 'GET', + '/demo', + { + 'Host': 'example.com', + 'Connection': 'UPGRADE', + 'Sec-WebSocket-Key2': _TEST_KEY2, + 'Sec-WebSocket-Protocol': 'sample', + 'Upgrade': 'WEBSOCKET', + 'Sec-WebSocket-Key1': _TEST_KEY1, + 'Origin': 'http://example.com', + }, + _TEST_KEY3) + +_GOOD_REQUEST_CASE_MIXED_HEADER_NAMES = ( + 80, + 'GET', + '/demo', + { + 'hOsT': 'example.com', + 'cOnNeCtIoN': 'Upgrade', + 'sEc-wEbsOcKeT-kEy2': _TEST_KEY2, + 'sEc-wEbsOcKeT-pRoToCoL': 'sample', + 'uPgRaDe': 'WebSocket', + 'sEc-wEbsOcKeT-kEy1': _TEST_KEY1, + 'oRiGiN': 'http://example.com', + }, + _TEST_KEY3) + +_GOOD_RESPONSE_DEFAULT_PORT = ( + 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n' + 'Upgrade: WebSocket\r\n' + 'Connection: Upgrade\r\n' + 'Sec-WebSocket-Location: ws://example.com/demo\r\n' + 'Sec-WebSocket-Origin: http://example.com\r\n' + 'Sec-WebSocket-Protocol: sample\r\n' + '\r\n' + + _TEST_CHALLENGE_RESPONSE) + +_GOOD_RESPONSE_SECURE = ( + 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n' + 'Upgrade: WebSocket\r\n' + 'Connection: Upgrade\r\n' + 'Sec-WebSocket-Location: wss://example.com/demo\r\n' + 'Sec-WebSocket-Origin: http://example.com\r\n' + 'Sec-WebSocket-Protocol: sample\r\n' + '\r\n' + + _TEST_CHALLENGE_RESPONSE) + +_GOOD_REQUEST_NONDEFAULT_PORT = ( + 8081, + 'GET', + '/demo', + { + 'Host': 'example.com:8081', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key2': _TEST_KEY2, + 'Sec-WebSocket-Protocol': 'sample', + 'Upgrade': 'WebSocket', + 'Sec-WebSocket-Key1': _TEST_KEY1, + 'Origin': 'http://example.com', + }, + _TEST_KEY3) + +_GOOD_RESPONSE_NONDEFAULT_PORT = ( + 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n' + 'Upgrade: WebSocket\r\n' + 'Connection: Upgrade\r\n' + 'Sec-WebSocket-Location: ws://example.com:8081/demo\r\n' + 'Sec-WebSocket-Origin: http://example.com\r\n' + 'Sec-WebSocket-Protocol: sample\r\n' + '\r\n' + + _TEST_CHALLENGE_RESPONSE) + +_GOOD_RESPONSE_SECURE_NONDEF = ( + 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n' + 'Upgrade: WebSocket\r\n' + 'Connection: Upgrade\r\n' + 'Sec-WebSocket-Location: wss://example.com:8081/demo\r\n' + 'Sec-WebSocket-Origin: http://example.com\r\n' + 'Sec-WebSocket-Protocol: sample\r\n' + '\r\n' + + _TEST_CHALLENGE_RESPONSE) + +_GOOD_REQUEST_NO_PROTOCOL = ( + 80, + 'GET', + '/demo', + { + 'Host': 'example.com', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key2': _TEST_KEY2, + 'Upgrade': 'WebSocket', + 'Sec-WebSocket-Key1': _TEST_KEY1, + 'Origin': 'http://example.com', + }, + _TEST_KEY3) + +_GOOD_RESPONSE_NO_PROTOCOL = ( + 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n' + 'Upgrade: WebSocket\r\n' + 'Connection: Upgrade\r\n' + 'Sec-WebSocket-Location: ws://example.com/demo\r\n' + 'Sec-WebSocket-Origin: http://example.com\r\n' + '\r\n' + + _TEST_CHALLENGE_RESPONSE) + +_GOOD_REQUEST_WITH_OPTIONAL_HEADERS = ( + 80, + 'GET', + '/demo', + { + 'Host': 'example.com', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key2': _TEST_KEY2, + 'EmptyValue': '', + 'Sec-WebSocket-Protocol': 'sample', + 'AKey': 'AValue', + 'Upgrade': 'WebSocket', + 'Sec-WebSocket-Key1': _TEST_KEY1, + 'Origin': 'http://example.com', + }, + _TEST_KEY3) + +# TODO(tyoshino): Include \r \n in key3, challenge response. + +_GOOD_REQUEST_WITH_NONPRINTABLE_KEY = ( + 80, + 'GET', + '/demo', + { + 'Host': 'example.com', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key2': 'y R2 48 Q1O4 e|BV3 i5 1 u- 65', + 'Sec-WebSocket-Protocol': 'sample', + 'Upgrade': 'WebSocket', + 'Sec-WebSocket-Key1': '36 7 74 i 92 2\'m 9 0G', + 'Origin': 'http://example.com', + }, + ''.join(map(chr, [0x01, 0xd1, 0xdd, 0x3b, 0xd1, 0x56, 0x63, 0xff]))) + +_GOOD_RESPONSE_WITH_NONPRINTABLE_KEY = ( + 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n' + 'Upgrade: WebSocket\r\n' + 'Connection: Upgrade\r\n' + 'Sec-WebSocket-Location: ws://example.com/demo\r\n' + 'Sec-WebSocket-Origin: http://example.com\r\n' + 'Sec-WebSocket-Protocol: sample\r\n' + '\r\n' + + ''.join(map(chr, [0x0b, 0x99, 0xfa, 0x55, 0xbd, 0x01, 0x23, 0x7b, + 0x45, 0xa2, 0xf1, 0xd0, 0x87, 0x8a, 0xee, 0xeb]))) + +_GOOD_REQUEST_WITH_QUERY_PART = ( + 80, + 'GET', + '/demo?e=mc2', + { + 'Host': 'example.com', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key2': _TEST_KEY2, + 'Sec-WebSocket-Protocol': 'sample', + 'Upgrade': 'WebSocket', + 'Sec-WebSocket-Key1': _TEST_KEY1, + 'Origin': 'http://example.com', + }, + _TEST_KEY3) + +_GOOD_RESPONSE_WITH_QUERY_PART = ( + 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n' + 'Upgrade: WebSocket\r\n' + 'Connection: Upgrade\r\n' + 'Sec-WebSocket-Location: ws://example.com/demo?e=mc2\r\n' + 'Sec-WebSocket-Origin: http://example.com\r\n' + 'Sec-WebSocket-Protocol: sample\r\n' + '\r\n' + + _TEST_CHALLENGE_RESPONSE) + +_BAD_REQUESTS = ( + ( # HTTP request + 80, + 'GET', + '/demo', + { + 'Host': 'www.google.com', + 'User-Agent': 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5;' + ' en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3' + ' GTB6 GTBA', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,' + '*/*;q=0.8', + 'Accept-Language': 'en-us,en;q=0.5', + 'Accept-Encoding': 'gzip,deflate', + 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', + 'Keep-Alive': '300', + 'Connection': 'keep-alive', + }), + ( # Wrong method + 80, + 'POST', + '/demo', + { + 'Host': 'example.com', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key2': _TEST_KEY2, + 'Sec-WebSocket-Protocol': 'sample', + 'Upgrade': 'WebSocket', + 'Sec-WebSocket-Key1': _TEST_KEY1, + 'Origin': 'http://example.com', + }, + _TEST_KEY3), + ( # Missing Upgrade + 80, + 'GET', + '/demo', + { + 'Host': 'example.com', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key2': _TEST_KEY2, + 'Sec-WebSocket-Protocol': 'sample', + 'Sec-WebSocket-Key1': _TEST_KEY1, + 'Origin': 'http://example.com', + }, + _TEST_KEY3), + ( # Wrong Upgrade + 80, + 'GET', + '/demo', + { + 'Host': 'example.com', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key2': _TEST_KEY2, + 'Sec-WebSocket-Protocol': 'sample', + 'Upgrade': 'NonWebSocket', + 'Sec-WebSocket-Key1': _TEST_KEY1, + 'Origin': 'http://example.com', + }, + _TEST_KEY3), + ( # Empty WebSocket-Protocol + 80, + 'GET', + '/demo', + { + 'Host': 'example.com', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key2': _TEST_KEY2, + 'Sec-WebSocket-Protocol': '', + 'Upgrade': 'WebSocket', + 'Sec-WebSocket-Key1': _TEST_KEY1, + 'Origin': 'http://example.com', + }, + _TEST_KEY3), + ( # Wrong port number format + 80, + 'GET', + '/demo', + { + 'Host': 'example.com:0x50', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key2': _TEST_KEY2, + 'Sec-WebSocket-Protocol': 'sample', + 'Upgrade': 'WebSocket', + 'Sec-WebSocket-Key1': _TEST_KEY1, + 'Origin': 'http://example.com', + }, + _TEST_KEY3), + ( # Header/connection port mismatch + 8080, + 'GET', + '/demo', + { + 'Host': 'example.com', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key2': _TEST_KEY2, + 'Sec-WebSocket-Protocol': 'sample', + 'Upgrade': 'WebSocket', + 'Sec-WebSocket-Key1': _TEST_KEY1, + 'Origin': 'http://example.com', + }, + _TEST_KEY3), + ( # Illegal WebSocket-Protocol + 80, + 'GET', + '/demo', + { + 'Host': 'example.com', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key2': _TEST_KEY2, + 'Sec-WebSocket-Protocol': 'illegal\x09protocol', + 'Upgrade': 'WebSocket', + 'Sec-WebSocket-Key1': _TEST_KEY1, + 'Origin': 'http://example.com', + }, + _TEST_KEY3), +) + + +def _create_request(request_def): + data = '' + if len(request_def) > 4: + data = request_def[4] + conn = mock.MockConn(data) + conn.local_addr = ('0.0.0.0', request_def[0]) + return mock.MockRequest( + method=request_def[1], + uri=request_def[2], + headers_in=request_def[3], + connection=conn) + + +def _create_get_memorized_lines(lines): + """Creates a function that returns the given string.""" + + def get_memorized_lines(): + return lines + return get_memorized_lines + + +def _create_requests_with_lines(request_lines_set): + requests = [] + for lines in request_lines_set: + request = _create_request(_GOOD_REQUEST) + request.connection.get_memorized_lines = _create_get_memorized_lines( + lines) + requests.append(request) + return requests + + +class HyBi00HandshakerTest(unittest.TestCase): + + def test_good_request_default_port(self): + request = _create_request(_GOOD_REQUEST) + handshaker = Handshaker(request, mock.MockDispatcher()) + handshaker.do_handshake() + self.assertEqual(_GOOD_RESPONSE_DEFAULT_PORT, + request.connection.written_data()) + self.assertEqual('/demo', request.ws_resource) + self.assertEqual('http://example.com', request.ws_origin) + self.assertEqual('ws://example.com/demo', request.ws_location) + self.assertEqual('sample', request.ws_protocol) + + def test_good_request_capitalized_header_values(self): + request = _create_request(_GOOD_REQUEST_CAPITALIZED_HEADER_VALUES) + handshaker = Handshaker(request, mock.MockDispatcher()) + handshaker.do_handshake() + self.assertEqual(_GOOD_RESPONSE_DEFAULT_PORT, + request.connection.written_data()) + + def test_good_request_case_mixed_header_names(self): + request = _create_request(_GOOD_REQUEST_CASE_MIXED_HEADER_NAMES) + handshaker = Handshaker(request, mock.MockDispatcher()) + handshaker.do_handshake() + self.assertEqual(_GOOD_RESPONSE_DEFAULT_PORT, + request.connection.written_data()) + + def test_good_request_secure_default_port(self): + request = _create_request(_GOOD_REQUEST) + request.connection.local_addr = ('0.0.0.0', 443) + request.is_https_ = True + handshaker = Handshaker(request, mock.MockDispatcher()) + handshaker.do_handshake() + self.assertEqual(_GOOD_RESPONSE_SECURE, + request.connection.written_data()) + self.assertEqual('sample', request.ws_protocol) + + def test_good_request_nondefault_port(self): + request = _create_request(_GOOD_REQUEST_NONDEFAULT_PORT) + handshaker = Handshaker(request, + mock.MockDispatcher()) + handshaker.do_handshake() + self.assertEqual(_GOOD_RESPONSE_NONDEFAULT_PORT, + request.connection.written_data()) + self.assertEqual('sample', request.ws_protocol) + + def test_good_request_secure_non_default_port(self): + request = _create_request(_GOOD_REQUEST_NONDEFAULT_PORT) + request.is_https_ = True + handshaker = Handshaker(request, mock.MockDispatcher()) + handshaker.do_handshake() + self.assertEqual(_GOOD_RESPONSE_SECURE_NONDEF, + request.connection.written_data()) + self.assertEqual('sample', request.ws_protocol) + + def test_good_request_default_no_protocol(self): + request = _create_request(_GOOD_REQUEST_NO_PROTOCOL) + handshaker = Handshaker(request, mock.MockDispatcher()) + handshaker.do_handshake() + self.assertEqual(_GOOD_RESPONSE_NO_PROTOCOL, + request.connection.written_data()) + self.assertEqual(None, request.ws_protocol) + + def test_good_request_optional_headers(self): + request = _create_request(_GOOD_REQUEST_WITH_OPTIONAL_HEADERS) + handshaker = Handshaker(request, mock.MockDispatcher()) + handshaker.do_handshake() + self.assertEqual('AValue', + request.headers_in['AKey']) + self.assertEqual('', + request.headers_in['EmptyValue']) + + def test_good_request_with_nonprintable_key(self): + request = _create_request(_GOOD_REQUEST_WITH_NONPRINTABLE_KEY) + handshaker = Handshaker(request, mock.MockDispatcher()) + handshaker.do_handshake() + self.assertEqual(_GOOD_RESPONSE_WITH_NONPRINTABLE_KEY, + request.connection.written_data()) + self.assertEqual('sample', request.ws_protocol) + + def test_good_request_with_query_part(self): + request = _create_request(_GOOD_REQUEST_WITH_QUERY_PART) + handshaker = Handshaker(request, mock.MockDispatcher()) + handshaker.do_handshake() + self.assertEqual(_GOOD_RESPONSE_WITH_QUERY_PART, + request.connection.written_data()) + self.assertEqual('ws://example.com/demo?e=mc2', request.ws_location) + + def test_bad_requests(self): + for request in map(_create_request, _BAD_REQUESTS): + handshaker = Handshaker(request, mock.MockDispatcher()) + self.assertRaises(HandshakeException, handshaker.do_handshake) + + +class HyBi00ValidateSubprotocolTest(unittest.TestCase): + def test_validate_subprotocol(self): + # should succeed. + _validate_subprotocol('sample') + _validate_subprotocol('Sample') + _validate_subprotocol('sample\x7eprotocol') + _validate_subprotocol('sample\x20protocol') + + # should fail. + self.assertRaises(HandshakeException, + _validate_subprotocol, + '') + self.assertRaises(HandshakeException, + _validate_subprotocol, + 'sample\x19protocol') + self.assertRaises(HandshakeException, + _validate_subprotocol, + 'sample\x7fprotocol') + self.assertRaises(HandshakeException, + _validate_subprotocol, + # "Japan" in Japanese + u'\u65e5\u672c') + + +if __name__ == '__main__': + unittest.main() + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_http_header_util.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_http_header_util.py new file mode 100755 index 000000000..436dc57c3 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_http_header_util.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Tests for http_header_util module.""" + + +import unittest + +from mod_pywebsocket import http_header_util + + +class UnitTest(unittest.TestCase): + """A unittest for http_header_util module.""" + + def test_parse_relative_uri(self): + host, port, resource = http_header_util.parse_uri('/ws/test') + self.assertEqual(None, host) + self.assertEqual(None, port) + self.assertEqual('/ws/test', resource) + + def test_parse_absolute_uri(self): + host, port, resource = http_header_util.parse_uri( + 'ws://localhost:10080/ws/test') + self.assertEqual('localhost', host) + self.assertEqual(10080, port) + self.assertEqual('/ws/test', resource) + + host, port, resource = http_header_util.parse_uri( + 'ws://example.com/ws/test') + self.assertEqual('example.com', host) + self.assertEqual(80, port) + self.assertEqual('/ws/test', resource) + + host, port, resource = http_header_util.parse_uri( + 'wss://example.com/') + self.assertEqual('example.com', host) + self.assertEqual(443, port) + self.assertEqual('/', resource) + + host, port, resource = http_header_util.parse_uri( + 'ws://example.com:8080') + self.assertEqual('example.com', host) + self.assertEqual(8080, port) + self.assertEqual('/', resource) + + def test_parse_invalid_uri(self): + host, port, resource = http_header_util.parse_uri('ws:///') + self.assertEqual(None, resource) + + host, port, resource = http_header_util.parse_uri('ws://localhost:') + self.assertEqual(None, resource) + + host, port, resource = http_header_util.parse_uri('ws://localhost:/ws') + self.assertEqual(None, resource) + + +if __name__ == '__main__': + unittest.main() + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_memorizingfile.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_memorizingfile.py new file mode 100755 index 000000000..8f1b8eef4 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_memorizingfile.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Tests for memorizingfile module.""" + + +import StringIO +import unittest + +import set_sys_path # Update sys.path to locate mod_pywebsocket module. + +from mod_pywebsocket import memorizingfile + + +class UtilTest(unittest.TestCase): + """A unittest for memorizingfile module.""" + + def check(self, memorizing_file, num_read, expected_list): + for unused in range(num_read): + memorizing_file.readline() + actual_list = memorizing_file.get_memorized_lines() + self.assertEqual(len(expected_list), len(actual_list)) + for expected, actual in zip(expected_list, actual_list): + self.assertEqual(expected, actual) + + def check_with_size(self, memorizing_file, read_size, expected_list): + read_list = [] + read_line = '' + while True: + line = memorizing_file.readline(read_size) + line_length = len(line) + self.assertTrue(line_length <= read_size) + if line_length == 0: + if read_line != '': + read_list.append(read_line) + break + read_line += line + if line[line_length - 1] == '\n': + read_list.append(read_line) + read_line = '' + actual_list = memorizing_file.get_memorized_lines() + self.assertEqual(len(expected_list), len(actual_list)) + self.assertEqual(len(expected_list), len(read_list)) + for expected, actual, read in zip(expected_list, actual_list, + read_list): + self.assertEqual(expected, actual) + self.assertEqual(expected, read) + + def test_get_memorized_lines(self): + memorizing_file = memorizingfile.MemorizingFile(StringIO.StringIO( + 'Hello\nWorld\nWelcome')) + self.check(memorizing_file, 3, ['Hello\n', 'World\n', 'Welcome']) + + def test_get_memorized_lines_limit_memorized_lines(self): + memorizing_file = memorizingfile.MemorizingFile(StringIO.StringIO( + 'Hello\nWorld\nWelcome'), 2) + self.check(memorizing_file, 3, ['Hello\n', 'World\n']) + + def test_get_memorized_lines_empty_file(self): + memorizing_file = memorizingfile.MemorizingFile(StringIO.StringIO( + '')) + self.check(memorizing_file, 10, []) + + def test_get_memorized_lines_with_size(self): + for size in range(1, 10): + memorizing_file = memorizingfile.MemorizingFile(StringIO.StringIO( + 'Hello\nWorld\nWelcome')) + self.check_with_size(memorizing_file, size, + ['Hello\n', 'World\n', 'Welcome']) + +if __name__ == '__main__': + unittest.main() + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_mock.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_mock.py new file mode 100755 index 000000000..7dc23a73d --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_mock.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python +# +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Tests for mock module.""" + + +import Queue +import threading +import unittest + +import set_sys_path # Update sys.path to locate mod_pywebsocket module. + +from test import mock + + +class MockConnTest(unittest.TestCase): + """A unittest for MockConn class.""" + + def setUp(self): + self._conn = mock.MockConn('ABC\r\nDEFG\r\n\r\nHIJK') + + def test_readline(self): + self.assertEqual('ABC\r\n', self._conn.readline()) + self.assertEqual('DEFG\r\n', self._conn.readline()) + self.assertEqual('\r\n', self._conn.readline()) + self.assertEqual('HIJK', self._conn.readline()) + self.assertEqual('', self._conn.readline()) + + def test_read(self): + self.assertEqual('ABC\r\nD', self._conn.read(6)) + self.assertEqual('EFG\r\n\r\nHI', self._conn.read(9)) + self.assertEqual('JK', self._conn.read(10)) + self.assertEqual('', self._conn.read(10)) + + def test_read_and_readline(self): + self.assertEqual('ABC\r\nD', self._conn.read(6)) + self.assertEqual('EFG\r\n', self._conn.readline()) + self.assertEqual('\r\nHIJK', self._conn.read(9)) + self.assertEqual('', self._conn.readline()) + + def test_write(self): + self._conn.write('Hello\r\n') + self._conn.write('World\r\n') + self.assertEqual('Hello\r\nWorld\r\n', self._conn.written_data()) + + +class MockBlockingConnTest(unittest.TestCase): + """A unittest for MockBlockingConn class.""" + + def test_read(self): + """Tests that data put to MockBlockingConn by put_bytes method can be + read from it. + """ + + class LineReader(threading.Thread): + """A test class that launches a thread, calls readline on the + specified conn repeatedly and puts the read data to the specified + queue. + """ + + def __init__(self, conn, queue): + threading.Thread.__init__(self) + self._queue = queue + self._conn = conn + self.setDaemon(True) + self.start() + + def run(self): + while True: + data = self._conn.readline() + self._queue.put(data) + + conn = mock.MockBlockingConn() + queue = Queue.Queue() + reader = LineReader(conn, queue) + self.failUnless(queue.empty()) + conn.put_bytes('Foo bar\r\n') + read = queue.get() + self.assertEqual('Foo bar\r\n', read) + + +class MockTableTest(unittest.TestCase): + """A unittest for MockTable class.""" + + def test_create_from_dict(self): + table = mock.MockTable({'Key': 'Value'}) + self.assertEqual('Value', table.get('KEY')) + self.assertEqual('Value', table['key']) + + def test_create_from_list(self): + table = mock.MockTable([('Key', 'Value')]) + self.assertEqual('Value', table.get('KEY')) + self.assertEqual('Value', table['key']) + + def test_create_from_tuple(self): + table = mock.MockTable((('Key', 'Value'),)) + self.assertEqual('Value', table.get('KEY')) + self.assertEqual('Value', table['key']) + + def test_set_and_get(self): + table = mock.MockTable() + self.assertEqual(None, table.get('Key')) + table['Key'] = 'Value' + self.assertEqual('Value', table.get('Key')) + self.assertEqual('Value', table.get('key')) + self.assertEqual('Value', table.get('KEY')) + self.assertEqual('Value', table['Key']) + self.assertEqual('Value', table['key']) + self.assertEqual('Value', table['KEY']) + + +if __name__ == '__main__': + unittest.main() + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_msgutil.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_msgutil.py new file mode 100755 index 000000000..5fedcf92f --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_msgutil.py @@ -0,0 +1,1356 @@ +#!/usr/bin/env python +# +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Tests for msgutil module.""" + + +import array +import Queue +import random +import struct +import unittest +import zlib + +import set_sys_path # Update sys.path to locate mod_pywebsocket module. + +from mod_pywebsocket import common +from mod_pywebsocket.extensions import DeflateFrameExtensionProcessor +from mod_pywebsocket.extensions import PerMessageCompressExtensionProcessor +from mod_pywebsocket.extensions import PerMessageDeflateExtensionProcessor +from mod_pywebsocket import msgutil +from mod_pywebsocket.stream import InvalidUTF8Exception +from mod_pywebsocket.stream import Stream +from mod_pywebsocket.stream import StreamHixie75 +from mod_pywebsocket.stream import StreamOptions +from mod_pywebsocket import util +from test import mock + + +# We use one fixed nonce for testing instead of cryptographically secure PRNG. +_MASKING_NONCE = 'ABCD' + + +def _mask_hybi(frame): + frame_key = map(ord, _MASKING_NONCE) + frame_key_len = len(frame_key) + result = array.array('B') + result.fromstring(frame) + count = 0 + for i in xrange(len(result)): + result[i] ^= frame_key[count] + count = (count + 1) % frame_key_len + return _MASKING_NONCE + result.tostring() + + +def _install_extension_processor(processor, request, stream_options): + response = processor.get_extension_response() + if response is not None: + processor.setup_stream_options(stream_options) + request.ws_extension_processors.append(processor) + + +def _create_request_from_rawdata( + read_data, + deflate_frame_request=None, + permessage_compression_request=None, + permessage_deflate_request=None): + req = mock.MockRequest(connection=mock.MockConn(''.join(read_data))) + req.ws_version = common.VERSION_HYBI_LATEST + req.ws_extension_processors = [] + + processor = None + if deflate_frame_request is not None: + processor = DeflateFrameExtensionProcessor(deflate_frame_request) + elif permessage_compression_request is not None: + processor = PerMessageCompressExtensionProcessor( + permessage_compression_request) + elif permessage_deflate_request is not None: + processor = PerMessageDeflateExtensionProcessor( + permessage_deflate_request) + + stream_options = StreamOptions() + if processor is not None: + _install_extension_processor(processor, req, stream_options) + req.ws_stream = Stream(req, stream_options) + + return req + + +def _create_request(*frames): + """Creates MockRequest using data given as frames. + + frames will be returned on calling request.connection.read() where request + is MockRequest returned by this function. + """ + + read_data = [] + for (header, body) in frames: + read_data.append(header + _mask_hybi(body)) + + return _create_request_from_rawdata(read_data) + + +def _create_blocking_request(): + """Creates MockRequest. + + Data written to a MockRequest can be read out by calling + request.connection.written_data(). + """ + + req = mock.MockRequest(connection=mock.MockBlockingConn()) + req.ws_version = common.VERSION_HYBI_LATEST + stream_options = StreamOptions() + req.ws_stream = Stream(req, stream_options) + return req + + +def _create_request_hixie75(read_data=''): + req = mock.MockRequest(connection=mock.MockConn(read_data)) + req.ws_stream = StreamHixie75(req) + return req + + +def _create_blocking_request_hixie75(): + req = mock.MockRequest(connection=mock.MockBlockingConn()) + req.ws_stream = StreamHixie75(req) + return req + + +class BasicMessageTest(unittest.TestCase): + """Basic tests for Stream.""" + + def test_send_message(self): + request = _create_request() + msgutil.send_message(request, 'Hello') + self.assertEqual('\x81\x05Hello', request.connection.written_data()) + + payload = 'a' * 125 + request = _create_request() + msgutil.send_message(request, payload) + self.assertEqual('\x81\x7d' + payload, + request.connection.written_data()) + + def test_send_medium_message(self): + payload = 'a' * 126 + request = _create_request() + msgutil.send_message(request, payload) + self.assertEqual('\x81\x7e\x00\x7e' + payload, + request.connection.written_data()) + + payload = 'a' * ((1 << 16) - 1) + request = _create_request() + msgutil.send_message(request, payload) + self.assertEqual('\x81\x7e\xff\xff' + payload, + request.connection.written_data()) + + def test_send_large_message(self): + payload = 'a' * (1 << 16) + request = _create_request() + msgutil.send_message(request, payload) + self.assertEqual('\x81\x7f\x00\x00\x00\x00\x00\x01\x00\x00' + payload, + request.connection.written_data()) + + def test_send_message_unicode(self): + request = _create_request() + msgutil.send_message(request, u'\u65e5') + # U+65e5 is encoded as e6,97,a5 in UTF-8 + self.assertEqual('\x81\x03\xe6\x97\xa5', + request.connection.written_data()) + + def test_send_message_fragments(self): + request = _create_request() + msgutil.send_message(request, 'Hello', False) + msgutil.send_message(request, ' ', False) + msgutil.send_message(request, 'World', False) + msgutil.send_message(request, '!', True) + self.assertEqual('\x01\x05Hello\x00\x01 \x00\x05World\x80\x01!', + request.connection.written_data()) + + def test_send_fragments_immediate_zero_termination(self): + request = _create_request() + msgutil.send_message(request, 'Hello World!', False) + msgutil.send_message(request, '', True) + self.assertEqual('\x01\x0cHello World!\x80\x00', + request.connection.written_data()) + + def test_receive_message(self): + request = _create_request( + ('\x81\x85', 'Hello'), ('\x81\x86', 'World!')) + self.assertEqual('Hello', msgutil.receive_message(request)) + self.assertEqual('World!', msgutil.receive_message(request)) + + payload = 'a' * 125 + request = _create_request(('\x81\xfd', payload)) + self.assertEqual(payload, msgutil.receive_message(request)) + + def test_receive_medium_message(self): + payload = 'a' * 126 + request = _create_request(('\x81\xfe\x00\x7e', payload)) + self.assertEqual(payload, msgutil.receive_message(request)) + + payload = 'a' * ((1 << 16) - 1) + request = _create_request(('\x81\xfe\xff\xff', payload)) + self.assertEqual(payload, msgutil.receive_message(request)) + + def test_receive_large_message(self): + payload = 'a' * (1 << 16) + request = _create_request( + ('\x81\xff\x00\x00\x00\x00\x00\x01\x00\x00', payload)) + self.assertEqual(payload, msgutil.receive_message(request)) + + def test_receive_length_not_encoded_using_minimal_number_of_bytes(self): + # Log warning on receiving bad payload length field that doesn't use + # minimal number of bytes but continue processing. + + payload = 'a' + # 1 byte can be represented without extended payload length field. + request = _create_request( + ('\x81\xff\x00\x00\x00\x00\x00\x00\x00\x01', payload)) + self.assertEqual(payload, msgutil.receive_message(request)) + + def test_receive_message_unicode(self): + request = _create_request(('\x81\x83', '\xe6\x9c\xac')) + # U+672c is encoded as e6,9c,ac in UTF-8 + self.assertEqual(u'\u672c', msgutil.receive_message(request)) + + def test_receive_message_erroneous_unicode(self): + # \x80 and \x81 are invalid as UTF-8. + request = _create_request(('\x81\x82', '\x80\x81')) + # Invalid characters should raise InvalidUTF8Exception + self.assertRaises(InvalidUTF8Exception, + msgutil.receive_message, + request) + + def test_receive_fragments(self): + request = _create_request( + ('\x01\x85', 'Hello'), + ('\x00\x81', ' '), + ('\x00\x85', 'World'), + ('\x80\x81', '!')) + self.assertEqual('Hello World!', msgutil.receive_message(request)) + + def test_receive_fragments_unicode(self): + # UTF-8 encodes U+6f22 into e6bca2 and U+5b57 into e5ad97. + request = _create_request( + ('\x01\x82', '\xe6\xbc'), + ('\x00\x82', '\xa2\xe5'), + ('\x80\x82', '\xad\x97')) + self.assertEqual(u'\u6f22\u5b57', msgutil.receive_message(request)) + + def test_receive_fragments_immediate_zero_termination(self): + request = _create_request( + ('\x01\x8c', 'Hello World!'), ('\x80\x80', '')) + self.assertEqual('Hello World!', msgutil.receive_message(request)) + + def test_receive_fragments_duplicate_start(self): + request = _create_request( + ('\x01\x85', 'Hello'), ('\x01\x85', 'World')) + self.assertRaises(msgutil.InvalidFrameException, + msgutil.receive_message, + request) + + def test_receive_fragments_intermediate_but_not_started(self): + request = _create_request(('\x00\x85', 'Hello')) + self.assertRaises(msgutil.InvalidFrameException, + msgutil.receive_message, + request) + + def test_receive_fragments_end_but_not_started(self): + request = _create_request(('\x80\x85', 'Hello')) + self.assertRaises(msgutil.InvalidFrameException, + msgutil.receive_message, + request) + + def test_receive_message_discard(self): + request = _create_request( + ('\x8f\x86', 'IGNORE'), ('\x81\x85', 'Hello'), + ('\x8f\x89', 'DISREGARD'), ('\x81\x86', 'World!')) + self.assertRaises(msgutil.UnsupportedFrameException, + msgutil.receive_message, request) + self.assertEqual('Hello', msgutil.receive_message(request)) + self.assertRaises(msgutil.UnsupportedFrameException, + msgutil.receive_message, request) + self.assertEqual('World!', msgutil.receive_message(request)) + + def test_receive_close(self): + request = _create_request( + ('\x88\x8a', struct.pack('!H', 1000) + 'Good bye')) + self.assertEqual(None, msgutil.receive_message(request)) + self.assertEqual(1000, request.ws_close_code) + self.assertEqual('Good bye', request.ws_close_reason) + + def test_send_longest_close(self): + reason = 'a' * 123 + request = _create_request( + ('\x88\xfd', + struct.pack('!H', common.STATUS_NORMAL_CLOSURE) + reason)) + request.ws_stream.close_connection(common.STATUS_NORMAL_CLOSURE, + reason) + self.assertEqual(request.ws_close_code, common.STATUS_NORMAL_CLOSURE) + self.assertEqual(request.ws_close_reason, reason) + + def test_send_close_too_long(self): + request = _create_request() + self.assertRaises(msgutil.BadOperationException, + Stream.close_connection, + request.ws_stream, + common.STATUS_NORMAL_CLOSURE, + 'a' * 124) + + def test_send_close_inconsistent_code_and_reason(self): + request = _create_request() + # reason parameter must not be specified when code is None. + self.assertRaises(msgutil.BadOperationException, + Stream.close_connection, + request.ws_stream, + None, + 'a') + + def test_send_ping(self): + request = _create_request() + msgutil.send_ping(request, 'Hello World!') + self.assertEqual('\x89\x0cHello World!', + request.connection.written_data()) + + def test_send_longest_ping(self): + request = _create_request() + msgutil.send_ping(request, 'a' * 125) + self.assertEqual('\x89\x7d' + 'a' * 125, + request.connection.written_data()) + + def test_send_ping_too_long(self): + request = _create_request() + self.assertRaises(msgutil.BadOperationException, + msgutil.send_ping, + request, + 'a' * 126) + + def test_receive_ping(self): + """Tests receiving a ping control frame.""" + + def handler(request, message): + request.called = True + + # Stream automatically respond to ping with pong without any action + # by application layer. + request = _create_request( + ('\x89\x85', 'Hello'), ('\x81\x85', 'World')) + self.assertEqual('World', msgutil.receive_message(request)) + self.assertEqual('\x8a\x05Hello', + request.connection.written_data()) + + request = _create_request( + ('\x89\x85', 'Hello'), ('\x81\x85', 'World')) + request.on_ping_handler = handler + self.assertEqual('World', msgutil.receive_message(request)) + self.assertTrue(request.called) + + def test_receive_longest_ping(self): + request = _create_request( + ('\x89\xfd', 'a' * 125), ('\x81\x85', 'World')) + self.assertEqual('World', msgutil.receive_message(request)) + self.assertEqual('\x8a\x7d' + 'a' * 125, + request.connection.written_data()) + + def test_receive_ping_too_long(self): + request = _create_request(('\x89\xfe\x00\x7e', 'a' * 126)) + self.assertRaises(msgutil.InvalidFrameException, + msgutil.receive_message, + request) + + def test_receive_pong(self): + """Tests receiving a pong control frame.""" + + def handler(request, message): + request.called = True + + request = _create_request( + ('\x8a\x85', 'Hello'), ('\x81\x85', 'World')) + request.on_pong_handler = handler + msgutil.send_ping(request, 'Hello') + self.assertEqual('\x89\x05Hello', + request.connection.written_data()) + # Valid pong is received, but receive_message won't return for it. + self.assertEqual('World', msgutil.receive_message(request)) + # Check that nothing was written after receive_message call. + self.assertEqual('\x89\x05Hello', + request.connection.written_data()) + + self.assertTrue(request.called) + + def test_receive_unsolicited_pong(self): + # Unsolicited pong is allowed from HyBi 07. + request = _create_request( + ('\x8a\x85', 'Hello'), ('\x81\x85', 'World')) + msgutil.receive_message(request) + + request = _create_request( + ('\x8a\x85', 'Hello'), ('\x81\x85', 'World')) + msgutil.send_ping(request, 'Jumbo') + # Body mismatch. + msgutil.receive_message(request) + + def test_ping_cannot_be_fragmented(self): + request = _create_request(('\x09\x85', 'Hello')) + self.assertRaises(msgutil.InvalidFrameException, + msgutil.receive_message, + request) + + def test_ping_with_too_long_payload(self): + request = _create_request(('\x89\xfe\x01\x00', 'a' * 256)) + self.assertRaises(msgutil.InvalidFrameException, + msgutil.receive_message, + request) + + +class DeflateFrameTest(unittest.TestCase): + """Tests for checking deflate-frame extension.""" + + def test_send_message(self): + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + + extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION) + request = _create_request_from_rawdata( + '', deflate_frame_request=extension) + msgutil.send_message(request, 'Hello') + msgutil.send_message(request, 'World') + + expected = '' + + compressed_hello = compress.compress('Hello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello = compressed_hello[:-4] + expected += '\xc1%c' % len(compressed_hello) + expected += compressed_hello + + compressed_world = compress.compress('World') + compressed_world += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_world = compressed_world[:-4] + expected += '\xc1%c' % len(compressed_world) + expected += compressed_world + + self.assertEqual(expected, request.connection.written_data()) + + def test_send_message_bfinal(self): + extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION) + request = _create_request_from_rawdata( + '', deflate_frame_request=extension) + self.assertEquals(1, len(request.ws_extension_processors)) + deflate_frame_processor = request.ws_extension_processors[0] + deflate_frame_processor.set_bfinal(True) + msgutil.send_message(request, 'Hello') + msgutil.send_message(request, 'World') + + expected = '' + + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + compressed_hello = compress.compress('Hello') + compressed_hello += compress.flush(zlib.Z_FINISH) + compressed_hello = compressed_hello + chr(0) + expected += '\xc1%c' % len(compressed_hello) + expected += compressed_hello + + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + compressed_world = compress.compress('World') + compressed_world += compress.flush(zlib.Z_FINISH) + compressed_world = compressed_world + chr(0) + expected += '\xc1%c' % len(compressed_world) + expected += compressed_world + + self.assertEqual(expected, request.connection.written_data()) + + def test_send_message_comp_bit(self): + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + + extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION) + request = _create_request_from_rawdata( + '', deflate_frame_request=extension) + self.assertEquals(1, len(request.ws_extension_processors)) + deflate_frame_processor = request.ws_extension_processors[0] + msgutil.send_message(request, 'Hello') + deflate_frame_processor.disable_outgoing_compression() + msgutil.send_message(request, 'Hello') + deflate_frame_processor.enable_outgoing_compression() + msgutil.send_message(request, 'Hello') + + expected = '' + + compressed_hello = compress.compress('Hello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello = compressed_hello[:-4] + expected += '\xc1%c' % len(compressed_hello) + expected += compressed_hello + + expected += '\x81\x05Hello' + + compressed_2nd_hello = compress.compress('Hello') + compressed_2nd_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_2nd_hello = compressed_2nd_hello[:-4] + expected += '\xc1%c' % len(compressed_2nd_hello) + expected += compressed_2nd_hello + + self.assertEqual(expected, request.connection.written_data()) + + def test_send_message_no_context_takeover_parameter(self): + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + + extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION) + extension.add_parameter('no_context_takeover', None) + request = _create_request_from_rawdata( + '', deflate_frame_request=extension) + for i in xrange(3): + msgutil.send_message(request, 'Hello') + + compressed_message = compress.compress('Hello') + compressed_message += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_message = compressed_message[:-4] + expected = '\xc1%c' % len(compressed_message) + expected += compressed_message + + self.assertEqual( + expected + expected + expected, request.connection.written_data()) + + def test_bad_request_parameters(self): + """Tests that if there's anything wrong with deflate-frame extension + request, deflate-frame is rejected. + """ + + extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION) + # max_window_bits less than 8 is illegal. + extension.add_parameter('max_window_bits', '7') + processor = DeflateFrameExtensionProcessor(extension) + self.assertEqual(None, processor.get_extension_response()) + + extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION) + # max_window_bits greater than 15 is illegal. + extension.add_parameter('max_window_bits', '16') + processor = DeflateFrameExtensionProcessor(extension) + self.assertEqual(None, processor.get_extension_response()) + + extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION) + # Non integer max_window_bits is illegal. + extension.add_parameter('max_window_bits', 'foobar') + processor = DeflateFrameExtensionProcessor(extension) + self.assertEqual(None, processor.get_extension_response()) + + extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION) + # no_context_takeover must not have any value. + extension.add_parameter('no_context_takeover', 'foobar') + processor = DeflateFrameExtensionProcessor(extension) + self.assertEqual(None, processor.get_extension_response()) + + def test_response_parameters(self): + extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION) + processor = DeflateFrameExtensionProcessor(extension) + processor.set_response_window_bits(8) + response = processor.get_extension_response() + self.assertTrue(response.has_parameter('max_window_bits')) + self.assertEqual('8', response.get_parameter_value('max_window_bits')) + + extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION) + processor = DeflateFrameExtensionProcessor(extension) + processor.set_response_no_context_takeover(True) + response = processor.get_extension_response() + self.assertTrue(response.has_parameter('no_context_takeover')) + self.assertTrue( + response.get_parameter_value('no_context_takeover') is None) + + def test_receive_message(self): + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + + data = '' + + compressed_hello = compress.compress('Hello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello = compressed_hello[:-4] + data += '\xc1%c' % (len(compressed_hello) | 0x80) + data += _mask_hybi(compressed_hello) + + compressed_websocket = compress.compress('WebSocket') + compressed_websocket += compress.flush(zlib.Z_FINISH) + compressed_websocket += '\x00' + data += '\xc1%c' % (len(compressed_websocket) | 0x80) + data += _mask_hybi(compressed_websocket) + + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + + compressed_world = compress.compress('World') + compressed_world += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_world = compressed_world[:-4] + data += '\xc1%c' % (len(compressed_world) | 0x80) + data += _mask_hybi(compressed_world) + + # Close frame + data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye') + + extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION) + request = _create_request_from_rawdata( + data, deflate_frame_request=extension) + self.assertEqual('Hello', msgutil.receive_message(request)) + self.assertEqual('WebSocket', msgutil.receive_message(request)) + self.assertEqual('World', msgutil.receive_message(request)) + + self.assertEqual(None, msgutil.receive_message(request)) + + def test_receive_message_client_using_smaller_window(self): + """Test that frames coming from a client which is using smaller window + size that the server are correctly received. + """ + + # Using the smallest window bits of 8 for generating input frames. + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -8) + + data = '' + + # Use a frame whose content is bigger than the clients' DEFLATE window + # size before compression. The content mainly consists of 'a' but + # repetition of 'b' is put at the head and tail so that if the window + # size is big, the head is back-referenced but if small, not. + payload = 'b' * 64 + 'a' * 1024 + 'b' * 64 + compressed_hello = compress.compress(payload) + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello = compressed_hello[:-4] + data += '\xc1%c' % (len(compressed_hello) | 0x80) + data += _mask_hybi(compressed_hello) + + # Close frame + data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye') + + extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION) + request = _create_request_from_rawdata( + data, deflate_frame_request=extension) + self.assertEqual(payload, msgutil.receive_message(request)) + + self.assertEqual(None, msgutil.receive_message(request)) + + def test_receive_message_comp_bit(self): + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + + data = '' + + compressed_hello = compress.compress('Hello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello = compressed_hello[:-4] + data += '\xc1%c' % (len(compressed_hello) | 0x80) + data += _mask_hybi(compressed_hello) + + data += '\x81\x85' + _mask_hybi('Hello') + + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + + compressed_2nd_hello = compress.compress('Hello') + compressed_2nd_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_2nd_hello = compressed_2nd_hello[:-4] + data += '\xc1%c' % (len(compressed_2nd_hello) | 0x80) + data += _mask_hybi(compressed_2nd_hello) + + extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION) + request = _create_request_from_rawdata( + data, deflate_frame_request=extension) + for i in xrange(3): + self.assertEqual('Hello', msgutil.receive_message(request)) + + def test_receive_message_various_btype(self): + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + + data = '' + + compressed_hello = compress.compress('Hello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello = compressed_hello[:-4] + data += '\xc1%c' % (len(compressed_hello) | 0x80) + data += _mask_hybi(compressed_hello) + + compressed_websocket = compress.compress('WebSocket') + compressed_websocket += compress.flush(zlib.Z_FINISH) + compressed_websocket += '\x00' + data += '\xc1%c' % (len(compressed_websocket) | 0x80) + data += _mask_hybi(compressed_websocket) + + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + + compressed_world = compress.compress('World') + compressed_world += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_world = compressed_world[:-4] + data += '\xc1%c' % (len(compressed_world) | 0x80) + data += _mask_hybi(compressed_world) + + # Close frame + data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye') + + extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION) + request = _create_request_from_rawdata( + data, deflate_frame_request=extension) + self.assertEqual('Hello', msgutil.receive_message(request)) + self.assertEqual('WebSocket', msgutil.receive_message(request)) + self.assertEqual('World', msgutil.receive_message(request)) + + self.assertEqual(None, msgutil.receive_message(request)) + + +class PerMessageDeflateTest(unittest.TestCase): + """Tests for permessage-deflate extension.""" + + def test_send_message(self): + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + request = _create_request_from_rawdata( + '', permessage_deflate_request=extension) + msgutil.send_message(request, 'Hello') + + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + compressed_hello = compress.compress('Hello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello = compressed_hello[:-4] + expected = '\xc1%c' % len(compressed_hello) + expected += compressed_hello + self.assertEqual(expected, request.connection.written_data()) + + def test_send_empty_message(self): + """Test that an empty message is compressed correctly.""" + + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + request = _create_request_from_rawdata( + '', permessage_deflate_request=extension) + + msgutil.send_message(request, '') + + # Payload in binary: 0b00000010 0b00000000 + # From LSB, + # - 1 bit of BFINAL (0) + # - 2 bits of BTYPE (01 that means fixed Huffman) + # - 7 bits of the first code (0000000 that is the code for the + # end-of-block) + # - 1 bit of BFINAL (0) + # - 2 bits of BTYPE (no compression) + # - 3 bits of padding + self.assertEqual('\xc1\x02\x02\x00', + request.connection.written_data()) + + def test_send_message_with_null_character(self): + """Test that a simple payload (one null) is framed correctly.""" + + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + request = _create_request_from_rawdata( + '', permessage_deflate_request=extension) + + msgutil.send_message(request, '\x00') + + # Payload in binary: 0b01100010 0b00000000 0b00000000 + # From LSB, + # - 1 bit of BFINAL (0) + # - 2 bits of BTYPE (01 that means fixed Huffman) + # - 8 bits of the first code (00110000 that is the code for the literal + # alphabet 0x00) + # - 7 bits of the second code (0000000 that is the code for the + # end-of-block) + # - 1 bit of BFINAL (0) + # - 2 bits of BTYPE (no compression) + # - 2 bits of padding + self.assertEqual('\xc1\x03\x62\x00\x00', + request.connection.written_data()) + + def test_send_two_messages(self): + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + request = _create_request_from_rawdata( + '', permessage_deflate_request=extension) + msgutil.send_message(request, 'Hello') + msgutil.send_message(request, 'World') + + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + + expected = '' + + compressed_hello = compress.compress('Hello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello = compressed_hello[:-4] + expected += '\xc1%c' % len(compressed_hello) + expected += compressed_hello + + compressed_world = compress.compress('World') + compressed_world += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_world = compressed_world[:-4] + expected += '\xc1%c' % len(compressed_world) + expected += compressed_world + + self.assertEqual(expected, request.connection.written_data()) + + def test_send_message_fragmented(self): + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + request = _create_request_from_rawdata( + '', permessage_deflate_request=extension) + msgutil.send_message(request, 'Hello', end=False) + msgutil.send_message(request, 'Goodbye', end=False) + msgutil.send_message(request, 'World') + + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + compressed_hello = compress.compress('Hello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + expected = '\x41%c' % len(compressed_hello) + expected += compressed_hello + compressed_goodbye = compress.compress('Goodbye') + compressed_goodbye += compress.flush(zlib.Z_SYNC_FLUSH) + expected += '\x00%c' % len(compressed_goodbye) + expected += compressed_goodbye + compressed_world = compress.compress('World') + compressed_world += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_world = compressed_world[:-4] + expected += '\x80%c' % len(compressed_world) + expected += compressed_world + self.assertEqual(expected, request.connection.written_data()) + + def test_send_message_fragmented_empty_first_frame(self): + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + request = _create_request_from_rawdata( + '', permessage_deflate_request=extension) + msgutil.send_message(request, '', end=False) + msgutil.send_message(request, 'Hello') + + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + compressed_hello = compress.compress('') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + expected = '\x41%c' % len(compressed_hello) + expected += compressed_hello + compressed_empty = compress.compress('Hello') + compressed_empty += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_empty = compressed_empty[:-4] + expected += '\x80%c' % len(compressed_empty) + expected += compressed_empty + print '%r' % expected + self.assertEqual(expected, request.connection.written_data()) + + def test_send_message_fragmented_empty_last_frame(self): + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + request = _create_request_from_rawdata( + '', permessage_deflate_request=extension) + msgutil.send_message(request, 'Hello', end=False) + msgutil.send_message(request, '') + + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + compressed_hello = compress.compress('Hello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + expected = '\x41%c' % len(compressed_hello) + expected += compressed_hello + compressed_empty = compress.compress('') + compressed_empty += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_empty = compressed_empty[:-4] + expected += '\x80%c' % len(compressed_empty) + expected += compressed_empty + self.assertEqual(expected, request.connection.written_data()) + + def test_send_message_using_small_window(self): + common_part = 'abcdefghijklmnopqrstuvwxyz' + test_message = common_part + '-' * 30000 + common_part + + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + extension.add_parameter('server_max_window_bits', '8') + request = _create_request_from_rawdata( + '', permessage_deflate_request=extension) + msgutil.send_message(request, test_message) + + expected_websocket_header_size = 2 + expected_websocket_payload_size = 91 + + actual_frame = request.connection.written_data() + self.assertEqual(expected_websocket_header_size + + expected_websocket_payload_size, + len(actual_frame)) + actual_header = actual_frame[0:expected_websocket_header_size] + actual_payload = actual_frame[expected_websocket_header_size:] + + self.assertEqual( + '\xc1%c' % expected_websocket_payload_size, actual_header) + decompress = zlib.decompressobj(-8) + decompressed_message = decompress.decompress( + actual_payload + '\x00\x00\xff\xff') + decompressed_message += decompress.flush() + self.assertEqual(test_message, decompressed_message) + self.assertEqual(0, len(decompress.unused_data)) + self.assertEqual(0, len(decompress.unconsumed_tail)) + + def test_send_message_no_context_takeover_parameter(self): + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + extension.add_parameter('server_no_context_takeover', None) + request = _create_request_from_rawdata( + '', permessage_deflate_request=extension) + for i in xrange(3): + msgutil.send_message(request, 'Hello', end=False) + msgutil.send_message(request, 'Hello', end=True) + + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + + first_hello = compress.compress('Hello') + first_hello += compress.flush(zlib.Z_SYNC_FLUSH) + expected = '\x41%c' % len(first_hello) + expected += first_hello + second_hello = compress.compress('Hello') + second_hello += compress.flush(zlib.Z_SYNC_FLUSH) + second_hello = second_hello[:-4] + expected += '\x80%c' % len(second_hello) + expected += second_hello + + self.assertEqual( + expected + expected + expected, + request.connection.written_data()) + + def test_send_message_fragmented_bfinal(self): + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + request = _create_request_from_rawdata( + '', permessage_deflate_request=extension) + self.assertEquals(1, len(request.ws_extension_processors)) + request.ws_extension_processors[0].set_bfinal(True) + msgutil.send_message(request, 'Hello', end=False) + msgutil.send_message(request, 'World', end=True) + + expected = '' + + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + compressed_hello = compress.compress('Hello') + compressed_hello += compress.flush(zlib.Z_FINISH) + compressed_hello = compressed_hello + chr(0) + expected += '\x41%c' % len(compressed_hello) + expected += compressed_hello + + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + compressed_world = compress.compress('World') + compressed_world += compress.flush(zlib.Z_FINISH) + compressed_world = compressed_world + chr(0) + expected += '\x80%c' % len(compressed_world) + expected += compressed_world + + self.assertEqual(expected, request.connection.written_data()) + + def test_receive_message_deflate(self): + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + + compressed_hello = compress.compress('Hello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello = compressed_hello[:-4] + data = '\xc1%c' % (len(compressed_hello) | 0x80) + data += _mask_hybi(compressed_hello) + + # Close frame + data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye') + + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + request = _create_request_from_rawdata( + data, permessage_deflate_request=extension) + self.assertEqual('Hello', msgutil.receive_message(request)) + + self.assertEqual(None, msgutil.receive_message(request)) + + def test_receive_message_random_section(self): + """Test that a compressed message fragmented into lots of chunks is + correctly received. + """ + + random.seed(a=0) + payload = ''.join( + [chr(random.randint(0, 255)) for i in xrange(1000)]) + + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + compressed_payload = compress.compress(payload) + compressed_payload += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_payload = compressed_payload[:-4] + + # Fragment the compressed payload into lots of frames. + bytes_chunked = 0 + data = '' + frame_count = 0 + + chunk_sizes = [] + + while bytes_chunked < len(compressed_payload): + # Make sure that + # - the length of chunks are equal or less than 125 so that we can + # use 1 octet length header format for all frames. + # - at least 10 chunks are created. + chunk_size = random.randint( + 1, min(125, + len(compressed_payload) / 10, + len(compressed_payload) - bytes_chunked)) + chunk_sizes.append(chunk_size) + chunk = compressed_payload[ + bytes_chunked:bytes_chunked + chunk_size] + bytes_chunked += chunk_size + + first_octet = 0x00 + if len(data) == 0: + first_octet = first_octet | 0x42 + if bytes_chunked == len(compressed_payload): + first_octet = first_octet | 0x80 + + data += '%c%c' % (first_octet, chunk_size | 0x80) + data += _mask_hybi(chunk) + + frame_count += 1 + + print "Chunk sizes: %r" % chunk_sizes + self.assertTrue(len(chunk_sizes) > 10) + + # Close frame + data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye') + + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + request = _create_request_from_rawdata( + data, permessage_deflate_request=extension) + self.assertEqual(payload, msgutil.receive_message(request)) + + self.assertEqual(None, msgutil.receive_message(request)) + + def test_receive_two_messages(self): + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + + data = '' + + compressed_hello = compress.compress('HelloWebSocket') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello = compressed_hello[:-4] + split_position = len(compressed_hello) / 2 + data += '\x41%c' % (split_position | 0x80) + data += _mask_hybi(compressed_hello[:split_position]) + + data += '\x80%c' % ((len(compressed_hello) - split_position) | 0x80) + data += _mask_hybi(compressed_hello[split_position:]) + + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + + compressed_world = compress.compress('World') + compressed_world += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_world = compressed_world[:-4] + data += '\xc1%c' % (len(compressed_world) | 0x80) + data += _mask_hybi(compressed_world) + + # Close frame + data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye') + + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + request = _create_request_from_rawdata( + data, permessage_deflate_request=extension) + self.assertEqual('HelloWebSocket', msgutil.receive_message(request)) + self.assertEqual('World', msgutil.receive_message(request)) + + self.assertEqual(None, msgutil.receive_message(request)) + + def test_receive_message_mixed_btype(self): + """Test that a message compressed using lots of DEFLATE blocks with + various flush mode is correctly received. + """ + + random.seed(a=0) + payload = ''.join( + [chr(random.randint(0, 255)) for i in xrange(1000)]) + + compress = None + + # Fragment the compressed payload into lots of frames. + bytes_chunked = 0 + compressed_payload = '' + + chunk_sizes = [] + methods = [] + sync_used = False + finish_used = False + + while bytes_chunked < len(payload): + # Make sure at least 10 chunks are created. + chunk_size = random.randint( + 1, min(100, len(payload) - bytes_chunked)) + chunk_sizes.append(chunk_size) + chunk = payload[bytes_chunked:bytes_chunked + chunk_size] + + bytes_chunked += chunk_size + + if compress is None: + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, + zlib.DEFLATED, + -zlib.MAX_WBITS) + + if bytes_chunked == len(payload): + compressed_payload += compress.compress(chunk) + compressed_payload += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_payload = compressed_payload[:-4] + else: + method = random.randint(0, 1) + methods.append(method) + if method == 0: + compressed_payload += compress.compress(chunk) + compressed_payload += compress.flush(zlib.Z_SYNC_FLUSH) + sync_used = True + else: + compressed_payload += compress.compress(chunk) + compressed_payload += compress.flush(zlib.Z_FINISH) + compress = None + finish_used = True + + print "Chunk sizes: %r" % chunk_sizes + self.assertTrue(len(chunk_sizes) > 10) + print "Methods: %r" % methods + self.assertTrue(sync_used) + self.assertTrue(finish_used) + + self.assertTrue(125 < len(compressed_payload)) + self.assertTrue(len(compressed_payload) < 65536) + data = '\xc2\xfe' + struct.pack('!H', len(compressed_payload)) + data += _mask_hybi(compressed_payload) + + # Close frame + data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye') + + extension = common.ExtensionParameter( + common.PERMESSAGE_DEFLATE_EXTENSION) + request = _create_request_from_rawdata( + data, permessage_deflate_request=extension) + self.assertEqual(payload, msgutil.receive_message(request)) + + self.assertEqual(None, msgutil.receive_message(request)) + + +class PerMessageCompressTest(unittest.TestCase): + """Tests for checking permessage-compression extension.""" + + def test_deflate_response_parameters(self): + extension = common.ExtensionParameter( + common.PERMESSAGE_COMPRESSION_EXTENSION) + extension.add_parameter('method', 'deflate') + processor = PerMessageCompressExtensionProcessor(extension) + response = processor.get_extension_response() + self.assertEqual('deflate', + response.get_parameter_value('method')) + + extension = common.ExtensionParameter( + common.PERMESSAGE_COMPRESSION_EXTENSION) + extension.add_parameter('method', 'deflate') + processor = PerMessageCompressExtensionProcessor(extension) + + def _compression_processor_hook(compression_processor): + compression_processor.set_client_max_window_bits(8) + compression_processor.set_client_no_context_takeover(True) + processor.set_compression_processor_hook( + _compression_processor_hook) + response = processor.get_extension_response() + self.assertEqual( + 'deflate; client_max_window_bits=8; client_no_context_takeover', + response.get_parameter_value('method')) + + +class MessageTestHixie75(unittest.TestCase): + """Tests for draft-hixie-thewebsocketprotocol-76 stream class.""" + + def test_send_message(self): + request = _create_request_hixie75() + msgutil.send_message(request, 'Hello') + self.assertEqual('\x00Hello\xff', request.connection.written_data()) + + def test_send_message_unicode(self): + request = _create_request_hixie75() + msgutil.send_message(request, u'\u65e5') + # U+65e5 is encoded as e6,97,a5 in UTF-8 + self.assertEqual('\x00\xe6\x97\xa5\xff', + request.connection.written_data()) + + def test_receive_message(self): + request = _create_request_hixie75('\x00Hello\xff\x00World!\xff') + self.assertEqual('Hello', msgutil.receive_message(request)) + self.assertEqual('World!', msgutil.receive_message(request)) + + def test_receive_message_unicode(self): + request = _create_request_hixie75('\x00\xe6\x9c\xac\xff') + # U+672c is encoded as e6,9c,ac in UTF-8 + self.assertEqual(u'\u672c', msgutil.receive_message(request)) + + def test_receive_message_erroneous_unicode(self): + # \x80 and \x81 are invalid as UTF-8. + request = _create_request_hixie75('\x00\x80\x81\xff') + # Invalid characters should be replaced with + # U+fffd REPLACEMENT CHARACTER + self.assertEqual(u'\ufffd\ufffd', msgutil.receive_message(request)) + + def test_receive_message_discard(self): + request = _create_request_hixie75('\x80\x06IGNORE\x00Hello\xff' + '\x01DISREGARD\xff\x00World!\xff') + self.assertEqual('Hello', msgutil.receive_message(request)) + self.assertEqual('World!', msgutil.receive_message(request)) + + +class MessageReceiverTest(unittest.TestCase): + """Tests the Stream class using MessageReceiver.""" + + def test_queue(self): + request = _create_blocking_request() + receiver = msgutil.MessageReceiver(request) + + self.assertEqual(None, receiver.receive_nowait()) + + request.connection.put_bytes('\x81\x86' + _mask_hybi('Hello!')) + self.assertEqual('Hello!', receiver.receive()) + + def test_onmessage(self): + onmessage_queue = Queue.Queue() + + def onmessage_handler(message): + onmessage_queue.put(message) + + request = _create_blocking_request() + receiver = msgutil.MessageReceiver(request, onmessage_handler) + + request.connection.put_bytes('\x81\x86' + _mask_hybi('Hello!')) + self.assertEqual('Hello!', onmessage_queue.get()) + + +class MessageReceiverHixie75Test(unittest.TestCase): + """Tests the StreamHixie75 class using MessageReceiver.""" + + def test_queue(self): + request = _create_blocking_request_hixie75() + receiver = msgutil.MessageReceiver(request) + + self.assertEqual(None, receiver.receive_nowait()) + + request.connection.put_bytes('\x00Hello!\xff') + self.assertEqual('Hello!', receiver.receive()) + + def test_onmessage(self): + onmessage_queue = Queue.Queue() + + def onmessage_handler(message): + onmessage_queue.put(message) + + request = _create_blocking_request_hixie75() + receiver = msgutil.MessageReceiver(request, onmessage_handler) + + request.connection.put_bytes('\x00Hello!\xff') + self.assertEqual('Hello!', onmessage_queue.get()) + + +class MessageSenderTest(unittest.TestCase): + """Tests the Stream class using MessageSender.""" + + def test_send(self): + request = _create_blocking_request() + sender = msgutil.MessageSender(request) + + sender.send('World') + self.assertEqual('\x81\x05World', request.connection.written_data()) + + def test_send_nowait(self): + # Use a queue to check the bytes written by MessageSender. + # request.connection.written_data() cannot be used here because + # MessageSender runs in a separate thread. + send_queue = Queue.Queue() + + def write(bytes): + send_queue.put(bytes) + + request = _create_blocking_request() + request.connection.write = write + + sender = msgutil.MessageSender(request) + + sender.send_nowait('Hello') + sender.send_nowait('World') + self.assertEqual('\x81\x05Hello', send_queue.get()) + self.assertEqual('\x81\x05World', send_queue.get()) + + +class MessageSenderHixie75Test(unittest.TestCase): + """Tests the StreamHixie75 class using MessageSender.""" + + def test_send(self): + request = _create_blocking_request_hixie75() + sender = msgutil.MessageSender(request) + + sender.send('World') + self.assertEqual('\x00World\xff', request.connection.written_data()) + + def test_send_nowait(self): + # Use a queue to check the bytes written by MessageSender. + # request.connection.written_data() cannot be used here because + # MessageSender runs in a separate thread. + send_queue = Queue.Queue() + + def write(bytes): + send_queue.put(bytes) + + request = _create_blocking_request_hixie75() + request.connection.write = write + + sender = msgutil.MessageSender(request) + + sender.send_nowait('Hello') + sender.send_nowait('World') + self.assertEqual('\x00Hello\xff', send_queue.get()) + self.assertEqual('\x00World\xff', send_queue.get()) + + +if __name__ == '__main__': + unittest.main() + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_mux.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_mux.py new file mode 100644 index 000000000..d4598944e --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_mux.py @@ -0,0 +1,2089 @@ +#!/usr/bin/env python +# +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Tests for mux module.""" + +import Queue +import copy +import logging +import optparse +import struct +import sys +import unittest +import time +import zlib + +import set_sys_path # Update sys.path to locate mod_pywebsocket module. + +from mod_pywebsocket import common +from mod_pywebsocket import mux +from mod_pywebsocket._stream_base import ConnectionTerminatedException +from mod_pywebsocket._stream_base import UnsupportedFrameException +from mod_pywebsocket._stream_hybi import Frame +from mod_pywebsocket._stream_hybi import Stream +from mod_pywebsocket._stream_hybi import StreamOptions +from mod_pywebsocket._stream_hybi import create_binary_frame +from mod_pywebsocket._stream_hybi import create_close_frame +from mod_pywebsocket._stream_hybi import create_closing_handshake_body +from mod_pywebsocket._stream_hybi import parse_frame +from mod_pywebsocket.extensions import MuxExtensionProcessor + + +import mock + + +_TEST_HEADERS = {'Host': 'server.example.com', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': '13', + 'Origin': 'http://example.com'} + + +class _OutgoingChannelData(object): + def __init__(self): + self.messages = [] + self.control_messages = [] + + self.builder = mux._InnerMessageBuilder() + +class _MockMuxConnection(mock.MockBlockingConn): + """Mock class of mod_python connection for mux.""" + + def __init__(self): + mock.MockBlockingConn.__init__(self) + self._control_blocks = [] + self._channel_data = {} + + self._current_opcode = None + self._pending_fragments = [] + + self.server_close_code = None + + def write(self, data): + """Override MockBlockingConn.write.""" + + self._current_data = data + self._position = 0 + + def _receive_bytes(length): + if self._position + length > len(self._current_data): + raise ConnectionTerminatedException( + 'Failed to receive %d bytes from encapsulated ' + 'frame' % length) + data = self._current_data[self._position:self._position+length] + self._position += length + return data + + # Parse physical frames and assemble a message if the message is + # fragmented. + opcode, payload, fin, rsv1, rsv2, rsv3 = ( + parse_frame(_receive_bytes, unmask_receive=False)) + + self._pending_fragments.append(payload) + + if self._current_opcode is None: + if opcode == common.OPCODE_CONTINUATION: + raise Exception('Sending invalid continuation opcode') + self._current_opcode = opcode + else: + if opcode != common.OPCODE_CONTINUATION: + raise Exception('Sending invalid opcode %d' % opcode) + if not fin: + return + + inner_frame_data = ''.join(self._pending_fragments) + self._pending_fragments = [] + self._current_opcode = None + + # Handle a control message on the physical channel. + # TODO(bashi): Support other opcodes if needed. + if opcode == common.OPCODE_CLOSE: + if len(payload) >= 2: + self.server_close_code = struct.unpack('!H', payload[:2])[0] + close_body = create_closing_handshake_body( + common.STATUS_NORMAL_CLOSURE, '') + close_frame = create_close_frame(close_body, mask=True) + self.put_bytes(close_frame) + return + + # Parse the payload of the message on physical channel. + parser = mux._MuxFramePayloadParser(inner_frame_data) + channel_id = parser.read_channel_id() + if channel_id == mux._CONTROL_CHANNEL_ID: + self._control_blocks.extend(list(parser.read_control_blocks())) + return + + if not channel_id in self._channel_data: + self._channel_data[channel_id] = _OutgoingChannelData() + channel_data = self._channel_data[channel_id] + + # Parse logical frames and assemble an inner (logical) message. + (inner_fin, inner_rsv1, inner_rsv2, inner_rsv3, inner_opcode, + inner_payload) = parser.read_inner_frame() + inner_frame = Frame(inner_fin, inner_rsv1, inner_rsv2, inner_rsv3, + inner_opcode, inner_payload) + message = channel_data.builder.build(inner_frame) + if message is None: + return + + if (message.opcode == common.OPCODE_TEXT or + message.opcode == common.OPCODE_BINARY): + channel_data.messages.append(message.payload) + + self.on_data_message(message.payload) + else: + channel_data.control_messages.append( + {'opcode': message.opcode, + 'message': message.payload}) + + def on_data_message(self, message): + pass + + def get_written_control_blocks(self): + return self._control_blocks + + def get_written_messages(self, channel_id): + return self._channel_data[channel_id].messages + + def get_written_control_messages(self, channel_id): + return self._channel_data[channel_id].control_messages + + +class _FailOnWriteConnection(_MockMuxConnection): + """Specicialized version of _MockMuxConnection. Its write() method raises + an exception for testing when a data message is written. + """ + + def on_data_message(self, message): + """Override to raise an exception.""" + + raise Exception('Intentional failure') + + +class _ChannelEvent(object): + """A structure that records channel events.""" + + def __init__(self): + self.request = None + self.messages = [] + self.exception = None + self.client_initiated_closing = False + + +class _MuxMockDispatcher(object): + """Mock class of dispatch.Dispatcher for mux.""" + + def __init__(self): + self.channel_events = {} + + def do_extra_handshake(self, request): + if request.ws_requested_protocols is not None: + request.ws_protocol = request.ws_requested_protocols[0] + + def _do_echo(self, request, channel_events): + while True: + message = request.ws_stream.receive_message() + if message == None: + channel_events.client_initiated_closing = True + return + if message == 'Goodbye': + return + channel_events.messages.append(message) + # echo back + request.ws_stream.send_message(message) + + def _do_ping(self, request, channel_events): + request.ws_stream.send_ping('Ping!') + + def _do_ping_while_hello_world(self, request, channel_events): + request.ws_stream.send_message('Hello ', end=False) + request.ws_stream.send_ping('Ping!') + request.ws_stream.send_message('World!', end=True) + + def _do_two_ping_while_hello_world(self, request, channel_events): + request.ws_stream.send_message('Hello ', end=False) + request.ws_stream.send_ping('Ping!') + request.ws_stream.send_ping('Pong!') + request.ws_stream.send_message('World!', end=True) + + def transfer_data(self, request): + self.channel_events[request.channel_id] = _ChannelEvent() + self.channel_events[request.channel_id].request = request + + try: + # Note: more handler will be added. + if request.uri.endswith('echo'): + self._do_echo(request, + self.channel_events[request.channel_id]) + elif request.uri.endswith('ping'): + self._do_ping(request, + self.channel_events[request.channel_id]) + elif request.uri.endswith('two_ping_while_hello_world'): + self._do_two_ping_while_hello_world( + request, self.channel_events[request.channel_id]) + elif request.uri.endswith('ping_while_hello_world'): + self._do_ping_while_hello_world( + request, self.channel_events[request.channel_id]) + else: + raise ValueError('Cannot handle path %r' % request.path) + if not request.server_terminated: + request.ws_stream.close_connection() + except ConnectionTerminatedException, e: + self.channel_events[request.channel_id].exception = e + except Exception, e: + self.channel_events[request.channel_id].exception = e + raise + + +def _create_mock_request(connection=None, logical_channel_extensions=None): + if connection is None: + connection = _MockMuxConnection() + + request = mock.MockRequest(uri='/echo', + headers_in=_TEST_HEADERS, + connection=connection) + request.ws_stream = Stream(request, options=StreamOptions()) + request.mux_processor = MuxExtensionProcessor( + common.ExtensionParameter(common.MUX_EXTENSION)) + if logical_channel_extensions is not None: + request.mux_processor.set_extensions(logical_channel_extensions) + request.mux_processor.set_quota(8 * 1024) + return request + + +def _create_add_channel_request_frame(channel_id, encoding, encoded_handshake): + # Allow invalid encoding for testing. + first_byte = ((mux._MUX_OPCODE_ADD_CHANNEL_REQUEST << 5) | encoding) + payload = (chr(first_byte) + + mux._encode_channel_id(channel_id) + + mux._encode_number(len(encoded_handshake)) + + encoded_handshake) + return create_binary_frame( + (mux._encode_channel_id(mux._CONTROL_CHANNEL_ID) + payload), mask=True) + + +def _create_drop_channel_frame(channel_id, code=None, message=''): + payload = mux._create_drop_channel(channel_id, code, message) + return create_binary_frame( + (mux._encode_channel_id(mux._CONTROL_CHANNEL_ID) + payload), mask=True) + + +def _create_flow_control_frame(channel_id, replenished_quota): + payload = mux._create_flow_control(channel_id, replenished_quota) + return create_binary_frame( + (mux._encode_channel_id(mux._CONTROL_CHANNEL_ID) + payload), mask=True) + + +def _create_logical_frame(channel_id, message, opcode=common.OPCODE_BINARY, + fin=True, rsv1=False, rsv2=False, rsv3=False, + mask=True): + bits = chr((fin << 7) | (rsv1 << 6) | (rsv2 << 5) | (rsv3 << 4) | opcode) + payload = mux._encode_channel_id(channel_id) + bits + message + return create_binary_frame(payload, mask=True) + + +def _create_request_header(path='/echo', extensions=None): + headers = ( + 'GET %s HTTP/1.1\r\n' + 'Host: server.example.com\r\n' + 'Connection: Upgrade\r\n' + 'Origin: http://example.com\r\n') % path + if extensions: + headers += '%s: %s' % ( + common.SEC_WEBSOCKET_EXTENSIONS_HEADER, extensions) + return headers + + +class MuxTest(unittest.TestCase): + """A unittest for mux module.""" + + def test_channel_id_decode(self): + data = '\x00\x01\xbf\xff\xdf\xff\xff\xff\xff\xff\xff' + parser = mux._MuxFramePayloadParser(data) + channel_id = parser.read_channel_id() + self.assertEqual(0, channel_id) + channel_id = parser.read_channel_id() + self.assertEqual(1, channel_id) + channel_id = parser.read_channel_id() + self.assertEqual(2 ** 14 - 1, channel_id) + channel_id = parser.read_channel_id() + self.assertEqual(2 ** 21 - 1, channel_id) + channel_id = parser.read_channel_id() + self.assertEqual(2 ** 29 - 1, channel_id) + self.assertEqual(len(data), parser._read_position) + + def test_channel_id_encode(self): + encoded = mux._encode_channel_id(0) + self.assertEqual('\x00', encoded) + encoded = mux._encode_channel_id(2 ** 14 - 1) + self.assertEqual('\xbf\xff', encoded) + encoded = mux._encode_channel_id(2 ** 14) + self.assertEqual('\xc0@\x00', encoded) + encoded = mux._encode_channel_id(2 ** 21 - 1) + self.assertEqual('\xdf\xff\xff', encoded) + encoded = mux._encode_channel_id(2 ** 21) + self.assertEqual('\xe0 \x00\x00', encoded) + encoded = mux._encode_channel_id(2 ** 29 - 1) + self.assertEqual('\xff\xff\xff\xff', encoded) + # channel_id is too large + self.assertRaises(ValueError, + mux._encode_channel_id, + 2 ** 29) + + def test_read_multiple_control_blocks(self): + # Use AddChannelRequest because it can contain arbitrary length of data + data = ('\x00\x01\x01a' + '\x00\x02\x7d%s' + '\x00\x03\x7e\xff\xff%s' + '\x00\x04\x7f\x00\x00\x00\x00\x00\x01\x00\x00%s') % ( + 'a' * 0x7d, 'b' * 0xffff, 'c' * 0x10000) + parser = mux._MuxFramePayloadParser(data) + blocks = list(parser.read_control_blocks()) + self.assertEqual(4, len(blocks)) + + self.assertEqual(mux._MUX_OPCODE_ADD_CHANNEL_REQUEST, blocks[0].opcode) + self.assertEqual(1, blocks[0].channel_id) + self.assertEqual(1, len(blocks[0].encoded_handshake)) + + self.assertEqual(mux._MUX_OPCODE_ADD_CHANNEL_REQUEST, blocks[1].opcode) + self.assertEqual(2, blocks[1].channel_id) + self.assertEqual(0x7d, len(blocks[1].encoded_handshake)) + + self.assertEqual(mux._MUX_OPCODE_ADD_CHANNEL_REQUEST, blocks[2].opcode) + self.assertEqual(3, blocks[2].channel_id) + self.assertEqual(0xffff, len(blocks[2].encoded_handshake)) + + self.assertEqual(mux._MUX_OPCODE_ADD_CHANNEL_REQUEST, blocks[3].opcode) + self.assertEqual(4, blocks[3].channel_id) + self.assertEqual(0x10000, len(blocks[3].encoded_handshake)) + + self.assertEqual(len(data), parser._read_position) + + def test_read_add_channel_request(self): + data = '\x00\x01\x01a' + parser = mux._MuxFramePayloadParser(data) + blocks = list(parser.read_control_blocks()) + self.assertEqual(mux._MUX_OPCODE_ADD_CHANNEL_REQUEST, blocks[0].opcode) + self.assertEqual(1, blocks[0].channel_id) + self.assertEqual(1, len(blocks[0].encoded_handshake)) + + def test_read_drop_channel(self): + data = '\x60\x01\x00' + parser = mux._MuxFramePayloadParser(data) + blocks = list(parser.read_control_blocks()) + self.assertEqual(1, len(blocks)) + self.assertEqual(1, blocks[0].channel_id) + self.assertEqual(mux._MUX_OPCODE_DROP_CHANNEL, blocks[0].opcode) + self.assertEqual(None, blocks[0].drop_code) + self.assertEqual(0, len(blocks[0].drop_message)) + + data = '\x60\x02\x09\x03\xe8Success' + parser = mux._MuxFramePayloadParser(data) + blocks = list(parser.read_control_blocks()) + self.assertEqual(1, len(blocks)) + self.assertEqual(2, blocks[0].channel_id) + self.assertEqual(mux._MUX_OPCODE_DROP_CHANNEL, blocks[0].opcode) + self.assertEqual(1000, blocks[0].drop_code) + self.assertEqual('Success', blocks[0].drop_message) + + # Reason is too short. + data = '\x60\x01\x01\x00' + parser = mux._MuxFramePayloadParser(data) + self.assertRaises(mux.PhysicalConnectionError, + lambda: list(parser.read_control_blocks())) + + def test_read_flow_control(self): + data = '\x40\x01\x02' + parser = mux._MuxFramePayloadParser(data) + blocks = list(parser.read_control_blocks()) + self.assertEqual(1, len(blocks)) + self.assertEqual(1, blocks[0].channel_id) + self.assertEqual(mux._MUX_OPCODE_FLOW_CONTROL, blocks[0].opcode) + self.assertEqual(2, blocks[0].send_quota) + + def test_read_new_channel_slot(self): + data = '\x80\x01\x02\x02\x03' + parser = mux._MuxFramePayloadParser(data) + # TODO(bashi): Implement + self.assertRaises(mux.PhysicalConnectionError, + lambda: list(parser.read_control_blocks())) + + def test_read_invalid_number_field_in_control_block(self): + # No number field. + data = '' + parser = mux._MuxFramePayloadParser(data) + self.assertRaises(ValueError, parser._read_number) + + # The last two bytes are missing. + data = '\x7e' + parser = mux._MuxFramePayloadParser(data) + self.assertRaises(ValueError, parser._read_number) + + # Missing the last one byte. + data = '\x7f\x00\x00\x00\x00\x00\x01\x00' + parser = mux._MuxFramePayloadParser(data) + self.assertRaises(ValueError, parser._read_number) + + # The length of number field is too large. + data = '\x7f\xff\xff\xff\xff\xff\xff\xff\xff' + parser = mux._MuxFramePayloadParser(data) + self.assertRaises(ValueError, parser._read_number) + + # The msb of the first byte is set. + data = '\x80' + parser = mux._MuxFramePayloadParser(data) + self.assertRaises(ValueError, parser._read_number) + + # Using 3 bytes encoding for 125. + data = '\x7e\x00\x7d' + parser = mux._MuxFramePayloadParser(data) + self.assertRaises(ValueError, parser._read_number) + + # Using 9 bytes encoding for 0xffff + data = '\x7f\x00\x00\x00\x00\x00\x00\xff\xff' + parser = mux._MuxFramePayloadParser(data) + self.assertRaises(ValueError, parser._read_number) + + def test_read_invalid_size_and_contents(self): + # Only contain number field. + data = '\x01' + parser = mux._MuxFramePayloadParser(data) + self.assertRaises(mux.PhysicalConnectionError, + parser._read_size_and_contents) + + def test_create_add_channel_response(self): + data = mux._create_add_channel_response(channel_id=1, + encoded_handshake='FooBar', + encoding=0, + rejected=False) + self.assertEqual('\x20\x01\x06FooBar', data) + + data = mux._create_add_channel_response(channel_id=2, + encoded_handshake='Hello', + encoding=1, + rejected=True) + self.assertEqual('\x31\x02\x05Hello', data) + + def test_create_drop_channel(self): + data = mux._create_drop_channel(channel_id=1) + self.assertEqual('\x60\x01\x00', data) + + data = mux._create_drop_channel(channel_id=1, + code=2000, + message='error') + self.assertEqual('\x60\x01\x07\x07\xd0error', data) + + # reason must be empty if code is None + self.assertRaises(ValueError, + mux._create_drop_channel, + 1, None, 'FooBar') + + def test_parse_request_text(self): + request_text = _create_request_header() + command, path, version, headers = mux._parse_request_text(request_text) + self.assertEqual('GET', command) + self.assertEqual('/echo', path) + self.assertEqual('HTTP/1.1', version) + self.assertEqual(3, len(headers)) + self.assertEqual('server.example.com', headers['Host']) + self.assertEqual('http://example.com', headers['Origin']) + + +class MuxHandlerTest(unittest.TestCase): + + def test_add_channel(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=6) + request.connection.put_bytes(flow_control) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=3, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=3, + replenished_quota=6) + request.connection.put_bytes(flow_control) + + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Hello')) + request.connection.put_bytes( + _create_logical_frame(channel_id=3, message='World')) + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Goodbye')) + request.connection.put_bytes( + _create_logical_frame(channel_id=3, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + self.assertEqual([], dispatcher.channel_events[1].messages) + self.assertEqual(['Hello'], dispatcher.channel_events[2].messages) + self.assertEqual(['World'], dispatcher.channel_events[3].messages) + # Channel 2 + messages = request.connection.get_written_messages(2) + self.assertEqual(1, len(messages)) + self.assertEqual('Hello', messages[0]) + # Channel 3 + messages = request.connection.get_written_messages(3) + self.assertEqual(1, len(messages)) + self.assertEqual('World', messages[0]) + control_blocks = request.connection.get_written_control_blocks() + # There should be 8 control blocks: + # - 1 NewChannelSlot + # - 2 AddChannelResponses for channel id 2 and 3 + # - 6 FlowControls for channel id 1 (initialize), 'Hello', 'World', + # and 3 'Goodbye's + self.assertEqual(9, len(control_blocks)) + + def test_physical_connection_write_failure(self): + # Use _FailOnWriteConnection. + request = _create_mock_request(connection=_FailOnWriteConnection()) + + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + + # Let the worker echo back 'Hello'. It causes _FailOnWriteConnection + # raising an exception. + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Hello')) + + # Let the worker exit. This will be unnecessary when + # _LogicalConnection.write() is changed to throw an exception if + # woke up by on_writer_done. + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + + # All threads should be done. + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + def test_send_blocked(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + # On receiving this 'Hello', the server tries to echo back 'Hello', + # but it will be blocked since there's no send quota available for the + # channel 2. + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Hello')) + + # Wait until the worker is blocked due to send quota shortage. + time.sleep(1) + + # Close the channel 2. The worker should be notified of the end of + # writer thread and stop waiting for send quota to be replenished. + drop_channel = _create_drop_channel_frame(channel_id=2) + + request.connection.put_bytes(drop_channel) + + # Make sure the channel 1 is also closed. + drop_channel = _create_drop_channel_frame(channel_id=1) + request.connection.put_bytes(drop_channel) + + # All threads should be done. + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + def test_add_channel_delta_encoding(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + delta = 'GET /echo HTTP/1.1\r\n\r\n' + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=1, encoded_handshake=delta) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=6) + request.connection.put_bytes(flow_control) + + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Hello')) + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + self.assertEqual(['Hello'], dispatcher.channel_events[2].messages) + messages = request.connection.get_written_messages(2) + self.assertEqual(1, len(messages)) + self.assertEqual('Hello', messages[0]) + + def test_add_channel_delta_encoding_override(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + # Override Sec-WebSocket-Protocol. + delta = ('GET /echo HTTP/1.1\r\n' + 'Sec-WebSocket-Protocol: x-foo\r\n' + '\r\n') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=1, encoded_handshake=delta) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=6) + request.connection.put_bytes(flow_control) + + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Hello')) + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + self.assertEqual(['Hello'], dispatcher.channel_events[2].messages) + messages = request.connection.get_written_messages(2) + self.assertEqual(1, len(messages)) + self.assertEqual('Hello', messages[0]) + self.assertEqual('x-foo', + dispatcher.channel_events[2].request.ws_protocol) + + def test_add_channel_delta_after_identity(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + # Sec-WebSocket-Protocol is different from client's opening handshake + # of the physical connection. + # TODO(bashi): Remove Upgrade, Connection, Sec-WebSocket-Key and + # Sec-WebSocket-Version. + encoded_handshake = ( + 'GET /echo HTTP/1.1\r\n' + 'Host: server.example.com\r\n' + 'Sec-WebSocket-Protocol: x-foo\r\n' + 'Connection: Upgrade\r\n' + 'Origin: http://example.com\r\n' + '\r\n') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=6) + request.connection.put_bytes(flow_control) + + delta = 'GET /echo HTTP/1.1\r\n\r\n' + add_channel_request = _create_add_channel_request_frame( + channel_id=3, encoding=1, encoded_handshake=delta) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=3, + replenished_quota=6) + request.connection.put_bytes(flow_control) + + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Hello')) + request.connection.put_bytes( + _create_logical_frame(channel_id=3, message='World')) + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Goodbye')) + request.connection.put_bytes( + _create_logical_frame(channel_id=3, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + self.assertEqual([], dispatcher.channel_events[1].messages) + self.assertEqual(['Hello'], dispatcher.channel_events[2].messages) + self.assertEqual(['World'], dispatcher.channel_events[3].messages) + # Channel 2 + messages = request.connection.get_written_messages(2) + self.assertEqual(1, len(messages)) + self.assertEqual('Hello', messages[0]) + # Channel 3 + messages = request.connection.get_written_messages(3) + self.assertEqual(1, len(messages)) + self.assertEqual('World', messages[0]) + # Handshake base should be updated. + self.assertEqual( + 'x-foo', + mux_handler._handshake_base._headers['Sec-WebSocket-Protocol']) + + def test_add_channel_delta_remove_header(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + # Override handshake delta base. + encoded_handshake = ( + 'GET /echo HTTP/1.1\r\n' + 'Host: server.example.com\r\n' + 'Sec-WebSocket-Protocol: x-foo\r\n' + 'Connection: Upgrade\r\n' + 'Origin: http://example.com\r\n' + '\r\n') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=6) + request.connection.put_bytes(flow_control) + + # Remove Sec-WebSocket-Protocol header. + delta = ('GET /echo HTTP/1.1\r\n' + 'Sec-WebSocket-Protocol:' + '\r\n') + add_channel_request = _create_add_channel_request_frame( + channel_id=3, encoding=1, encoded_handshake=delta) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=3, + replenished_quota=6) + request.connection.put_bytes(flow_control) + + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Hello')) + request.connection.put_bytes( + _create_logical_frame(channel_id=3, message='World')) + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Goodbye')) + request.connection.put_bytes( + _create_logical_frame(channel_id=3, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + self.assertEqual([], dispatcher.channel_events[1].messages) + self.assertEqual(['Hello'], dispatcher.channel_events[2].messages) + self.assertEqual(['World'], dispatcher.channel_events[3].messages) + # Channel 2 + messages = request.connection.get_written_messages(2) + self.assertEqual(1, len(messages)) + self.assertEqual('Hello', messages[0]) + # Channel 3 + messages = request.connection.get_written_messages(3) + self.assertEqual(1, len(messages)) + self.assertEqual('World', messages[0]) + self.assertEqual( + 'x-foo', + dispatcher.channel_events[2].request.ws_protocol) + self.assertEqual( + None, + dispatcher.channel_events[3].request.ws_protocol) + + def test_add_channel_delta_encoding_permessage_compress(self): + # Enable permessage compress extension on the implicitly opened channel. + extensions = common.parse_extensions( + '%s; method=deflate' % common.PERMESSAGE_COMPRESSION_EXTENSION) + request = _create_mock_request( + logical_channel_extensions=extensions) + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + delta = 'GET /echo HTTP/1.1\r\n\r\n' + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=1, encoded_handshake=delta) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=20) + request.connection.put_bytes(flow_control) + + # Send compressed 'Hello' on logical channel 1 and 2. + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + compressed_hello = compress.compress('Hello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello = compressed_hello[:-4] + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message=compressed_hello, + rsv1=True)) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message=compressed_hello, + rsv1=True)) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + self.assertEqual(['Hello'], dispatcher.channel_events[1].messages) + self.assertEqual(['Hello'], dispatcher.channel_events[2].messages) + # Written 'Hello's should be compressed. + messages = request.connection.get_written_messages(1) + self.assertEqual(1, len(messages)) + self.assertEqual(compressed_hello, messages[0]) + messages = request.connection.get_written_messages(2) + self.assertEqual(1, len(messages)) + self.assertEqual(compressed_hello, messages[0]) + + def test_add_channel_delta_encoding_remove_extensions(self): + # Enable permessage compress extension on the implicitly opened channel. + extensions = common.parse_extensions( + '%s; method=deflate' % common.PERMESSAGE_COMPRESSION_EXTENSION) + request = _create_mock_request( + logical_channel_extensions=extensions) + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + # Remove permessage compress extension. + delta = ('GET /echo HTTP/1.1\r\n' + 'Sec-WebSocket-Extensions:\r\n' + '\r\n') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=1, encoded_handshake=delta) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=20) + request.connection.put_bytes(flow_control) + + # Send compressed message on logical channel 2. The message should + # be rejected (since rsv1 is set). + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + compressed_hello = compress.compress('Hello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello = compressed_hello[:-4] + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message=compressed_hello, + rsv1=True)) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + drop_channel = next( + b for b in request.connection.get_written_control_blocks() + if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL) + self.assertEqual(mux._DROP_CODE_NORMAL_CLOSURE, drop_channel.drop_code) + self.assertEqual(2, drop_channel.channel_id) + # UnsupportedFrameException should be raised on logical channel 2. + self.assertTrue(isinstance(dispatcher.channel_events[2].exception, + UnsupportedFrameException)) + + def test_add_channel_invalid_encoding(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=3, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + drop_channel = next( + b for b in request.connection.get_written_control_blocks() + if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL) + self.assertEqual(mux._DROP_CODE_UNKNOWN_REQUEST_ENCODING, + drop_channel.drop_code) + self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR, + request.connection.server_close_code) + + def test_add_channel_incomplete_handshake(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + incomplete_encoded_handshake = 'GET /echo HTTP/1.1' + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=incomplete_encoded_handshake) + request.connection.put_bytes(add_channel_request) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + self.assertTrue(1 in dispatcher.channel_events) + self.assertTrue(not 2 in dispatcher.channel_events) + + def test_add_channel_duplicate_channel_id(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + drop_channel = next( + b for b in request.connection.get_written_control_blocks() + if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL) + self.assertEqual(mux._DROP_CODE_CHANNEL_ALREADY_EXISTS, + drop_channel.drop_code) + self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR, + request.connection.server_close_code) + + def test_receive_drop_channel(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + drop_channel = _create_drop_channel_frame(channel_id=2) + request.connection.put_bytes(drop_channel) + + # Terminate implicitly opened channel. + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + exception = dispatcher.channel_events[2].exception + self.assertTrue(exception.__class__ == ConnectionTerminatedException) + + def test_receive_ping_frame(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=13) + request.connection.put_bytes(flow_control) + + ping_frame = _create_logical_frame(channel_id=2, + message='Hello World!', + opcode=common.OPCODE_PING) + request.connection.put_bytes(ping_frame) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + messages = request.connection.get_written_control_messages(2) + self.assertEqual(common.OPCODE_PONG, messages[0]['opcode']) + self.assertEqual('Hello World!', messages[0]['message']) + + def test_receive_fragmented_ping(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=13) + request.connection.put_bytes(flow_control) + + # Send a ping with message 'Hello world!' in two fragmented frames. + ping_frame1 = _create_logical_frame(channel_id=2, + message='Hello ', + fin=False, + opcode=common.OPCODE_PING) + request.connection.put_bytes(ping_frame1) + ping_frame2 = _create_logical_frame(channel_id=2, + message='World!', + fin=True, + opcode=common.OPCODE_CONTINUATION) + request.connection.put_bytes(ping_frame2) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + messages = request.connection.get_written_control_messages(2) + self.assertEqual(common.OPCODE_PONG, messages[0]['opcode']) + self.assertEqual('Hello World!', messages[0]['message']) + + def test_receive_fragmented_ping_while_receiving_fragmented_message(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=19) + request.connection.put_bytes(flow_control) + + # Send a fragmented frame of message 'Hello '. + hello = _create_logical_frame(channel_id=2, + message='Hello ', + fin=False) + request.connection.put_bytes(hello) + + # Before sending the last fragmented frame of the message, send a + # fragmented ping. + ping1 = _create_logical_frame(channel_id=2, + message='Pi', + fin=False, + opcode=common.OPCODE_PING) + request.connection.put_bytes(ping1) + ping2 = _create_logical_frame(channel_id=2, + message='ng!', + fin=True, + opcode=common.OPCODE_CONTINUATION) + request.connection.put_bytes(ping2) + + # Send the last fragmented frame of the message. + world = _create_logical_frame(channel_id=2, + message='World!', + fin=True, + opcode=common.OPCODE_CONTINUATION) + request.connection.put_bytes(world) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + messages = request.connection.get_written_messages(2) + self.assertEqual(['Hello World!'], messages) + control_messages = request.connection.get_written_control_messages(2) + self.assertEqual(common.OPCODE_PONG, control_messages[0]['opcode']) + self.assertEqual('Ping!', control_messages[0]['message']) + + def test_receive_two_ping_while_receiving_fragmented_message(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=25) + request.connection.put_bytes(flow_control) + + # Send a fragmented frame of message 'Hello '. + hello = _create_logical_frame(channel_id=2, + message='Hello ', + fin=False) + request.connection.put_bytes(hello) + + # Before sending the last fragmented frame of the message, send a + # fragmented ping and a non-fragmented ping. + ping1 = _create_logical_frame(channel_id=2, + message='Pi', + fin=False, + opcode=common.OPCODE_PING) + request.connection.put_bytes(ping1) + ping2 = _create_logical_frame(channel_id=2, + message='ng!', + fin=True, + opcode=common.OPCODE_CONTINUATION) + request.connection.put_bytes(ping2) + ping3 = _create_logical_frame(channel_id=2, + message='Pong!', + fin=True, + opcode=common.OPCODE_PING) + request.connection.put_bytes(ping3) + + # Send the last fragmented frame of the message. + world = _create_logical_frame(channel_id=2, + message='World!', + fin=True, + opcode=common.OPCODE_CONTINUATION) + request.connection.put_bytes(world) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + messages = request.connection.get_written_messages(2) + self.assertEqual(['Hello World!'], messages) + control_messages = request.connection.get_written_control_messages(2) + self.assertEqual(common.OPCODE_PONG, control_messages[0]['opcode']) + self.assertEqual('Ping!', control_messages[0]['message']) + self.assertEqual(common.OPCODE_PONG, control_messages[1]['opcode']) + self.assertEqual('Pong!', control_messages[1]['message']) + + def test_receive_message_while_receiving_fragmented_ping(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=19) + request.connection.put_bytes(flow_control) + + # Send a fragmented ping. + ping1 = _create_logical_frame(channel_id=2, + message='Pi', + fin=False, + opcode=common.OPCODE_PING) + request.connection.put_bytes(ping1) + + # Before sending the last fragmented ping, send a message. + # The logical channel (2) should be dropped. + message = _create_logical_frame(channel_id=2, + message='Hello world!', + fin=True) + request.connection.put_bytes(message) + + # Send the last fragmented frame of the message. + ping2 = _create_logical_frame(channel_id=2, + message='ng!', + fin=True, + opcode=common.OPCODE_CONTINUATION) + request.connection.put_bytes(ping2) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + drop_channel = next( + b for b in request.connection.get_written_control_blocks() + if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL) + self.assertEqual(2, drop_channel.channel_id) + # No message should be sent on channel 2. + self.assertRaises(KeyError, + request.connection.get_written_messages, + 2) + self.assertRaises(KeyError, + request.connection.get_written_control_messages, + 2) + + def test_send_ping(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/ping') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=6) + request.connection.put_bytes(flow_control) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + messages = request.connection.get_written_control_messages(2) + self.assertEqual(common.OPCODE_PING, messages[0]['opcode']) + self.assertEqual('Ping!', messages[0]['message']) + + def test_send_fragmented_ping(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/ping') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + # Replenish 3 bytes. This isn't enough to send the whole ping frame + # because the frame will have 5 bytes message('Ping!'). The frame + # should be fragmented. + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=3) + request.connection.put_bytes(flow_control) + + # Wait until the worker is blocked due to send quota shortage. + time.sleep(1) + + # Replenish remaining 2 + 1 bytes (including extra cost). + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=3) + request.connection.put_bytes(flow_control) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + messages = request.connection.get_written_control_messages(2) + self.assertEqual(common.OPCODE_PING, messages[0]['opcode']) + self.assertEqual('Ping!', messages[0]['message']) + + def test_send_fragmented_ping_while_sending_fragmented_message(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header( + path='/ping_while_hello_world') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + # Application will send: + # - text message 'Hello ' with fin=0 + # - ping with 'Ping!' message + # - text message 'World!' with fin=1 + # Replenish (6 + 1) + (2 + 1) bytes so that the ping will be + # fragmented on the logical channel. + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=10) + request.connection.put_bytes(flow_control) + + time.sleep(1) + + # Replenish remaining 3 + 6 bytes. + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=9) + request.connection.put_bytes(flow_control) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + messages = request.connection.get_written_messages(2) + self.assertEqual(['Hello World!'], messages) + control_messages = request.connection.get_written_control_messages(2) + self.assertEqual(common.OPCODE_PING, control_messages[0]['opcode']) + self.assertEqual('Ping!', control_messages[0]['message']) + + def test_send_fragmented_two_ping_while_sending_fragmented_message(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header( + path='/two_ping_while_hello_world') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + # Application will send: + # - text message 'Hello ' with fin=0 + # - ping with 'Ping!' message + # - ping with 'Pong!' message + # - text message 'World!' with fin=1 + # Replenish (6 + 1) + (2 + 1) bytes so that the first ping will be + # fragmented on the logical channel. + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=10) + request.connection.put_bytes(flow_control) + + time.sleep(1) + + # Replenish remaining 3 + (5 + 1) + 6 bytes. The second ping won't + # be fragmented on the logical channel. + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=15) + request.connection.put_bytes(flow_control) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + messages = request.connection.get_written_messages(2) + self.assertEqual(['Hello World!'], messages) + control_messages = request.connection.get_written_control_messages(2) + self.assertEqual(common.OPCODE_PING, control_messages[0]['opcode']) + self.assertEqual('Ping!', control_messages[0]['message']) + self.assertEqual(common.OPCODE_PING, control_messages[1]['opcode']) + self.assertEqual('Pong!', control_messages[1]['message']) + + def test_send_drop_channel(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + + # DropChannel for channel id 1 which doesn't have reason. + frame = create_binary_frame('\x00\x60\x01\x00', mask=True) + request.connection.put_bytes(frame) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + drop_channel = next( + b for b in request.connection.get_written_control_blocks() + if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL) + self.assertEqual(mux._DROP_CODE_ACKNOWLEDGED, + drop_channel.drop_code) + self.assertEqual(1, drop_channel.channel_id) + + def test_two_flow_control(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + # Replenish 5 bytes. + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=5) + request.connection.put_bytes(flow_control) + + # Send 10 bytes. The server will try echo back 10 bytes. + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='HelloWorld')) + + # Replenish 5 + 1 (per-message extra cost) bytes. + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=6) + request.connection.put_bytes(flow_control) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + messages = request.connection.get_written_messages(2) + self.assertEqual(['HelloWorld'], messages) + received_flow_controls = [ + b for b in request.connection.get_written_control_blocks() + if b.opcode == mux._MUX_OPCODE_FLOW_CONTROL and b.channel_id == 2] + # Replenishment for 'HelloWorld' + 1 + self.assertEqual(11, received_flow_controls[0].send_quota) + # Replenishment for 'Goodbye' + 1 + self.assertEqual(8, received_flow_controls[1].send_quota) + + def test_no_send_quota_on_server(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='HelloWorld')) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + + # Just wait for 1 sec so that the server attempts to echo back + # 'HelloWorld'. + self.assertFalse(mux_handler.wait_until_done(timeout=1)) + + # No message should be sent on channel 2. + self.assertRaises(KeyError, + request.connection.get_written_messages, + 2) + + def test_no_send_quota_on_server_for_permessage_extra_cost(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=6) + request.connection.put_bytes(flow_control) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Hello')) + # Replenish only len('World') bytes. + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=5) + request.connection.put_bytes(flow_control) + # Server should not callback for this message. + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='World')) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + + # Just wait for 1 sec so that the server attempts to echo back + # 'World'. + self.assertFalse(mux_handler.wait_until_done(timeout=1)) + + # Only one message should be sent on channel 2. + messages = request.connection.get_written_messages(2) + self.assertEqual(['Hello'], messages) + + def test_quota_violation_by_client(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, 0) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='HelloWorld')) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + control_blocks = request.connection.get_written_control_blocks() + self.assertEqual(5, len(control_blocks)) + drop_channel = next( + b for b in control_blocks + if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL) + self.assertEqual(mux._DROP_CODE_SEND_QUOTA_VIOLATION, + drop_channel.drop_code) + + def test_consume_quota_empty_message(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + # Client has 1 byte quota. + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, 1) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=2) + request.connection.put_bytes(flow_control) + # Send an empty message. Pywebsocket always replenishes 1 byte quota + # for empty message + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='')) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + # This message violates quota on channel id 2. + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + self.assertEqual(1, len(dispatcher.channel_events[2].messages)) + self.assertEqual('', dispatcher.channel_events[2].messages[0]) + + received_flow_controls = [ + b for b in request.connection.get_written_control_blocks() + if b.opcode == mux._MUX_OPCODE_FLOW_CONTROL and b.channel_id == 2] + self.assertEqual(1, len(received_flow_controls)) + self.assertEqual(1, received_flow_controls[0].send_quota) + + drop_channel = next( + b for b in request.connection.get_written_control_blocks() + if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL) + self.assertEqual(2, drop_channel.channel_id) + self.assertEqual(mux._DROP_CODE_SEND_QUOTA_VIOLATION, + drop_channel.drop_code) + + def test_consume_quota_fragmented_message(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + # Client has len('Hello') + len('Goodbye') + 2 bytes quota. + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, 14) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=6) + request.connection.put_bytes(flow_control) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='He', fin=False, + opcode=common.OPCODE_TEXT)) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='llo', fin=True, + opcode=common.OPCODE_CONTINUATION)) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + messages = request.connection.get_written_messages(2) + self.assertEqual(['Hello'], messages) + + def test_fragmented_control_message(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/ping') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + # Replenish total 6 bytes in 3 FlowControls. + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=1) + request.connection.put_bytes(flow_control) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=2) + request.connection.put_bytes(flow_control) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=3) + request.connection.put_bytes(flow_control) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + messages = request.connection.get_written_control_messages(2) + self.assertEqual(common.OPCODE_PING, messages[0]['opcode']) + self.assertEqual('Ping!', messages[0]['message']) + + def test_channel_slot_violation_by_client(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(slots=1, + send_quota=mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=6) + request.connection.put_bytes(flow_control) + + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Hello')) + + # This request should be rejected. + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=3, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + flow_control = _create_flow_control_frame(channel_id=3, + replenished_quota=6) + request.connection.put_bytes(flow_control) + + request.connection.put_bytes( + _create_logical_frame(channel_id=3, message='Hello')) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + self.assertEqual([], dispatcher.channel_events[1].messages) + self.assertEqual(['Hello'], dispatcher.channel_events[2].messages) + self.assertFalse(dispatcher.channel_events.has_key(3)) + drop_channel = next( + b for b in request.connection.get_written_control_blocks() + if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL) + self.assertEqual(3, drop_channel.channel_id) + self.assertEqual(mux._DROP_CODE_NEW_CHANNEL_SLOT_VIOLATION, + drop_channel.drop_code) + + def test_quota_overflow_by_client(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(slots=1, + send_quota=mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + # Replenish 0x7FFFFFFFFFFFFFFF bytes twice. + flow_control = _create_flow_control_frame( + channel_id=2, + replenished_quota=0x7FFFFFFFFFFFFFFF) + request.connection.put_bytes(flow_control) + request.connection.put_bytes(flow_control) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + drop_channel = next( + b for b in request.connection.get_written_control_blocks() + if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL) + self.assertEqual(2, drop_channel.channel_id) + self.assertEqual(mux._DROP_CODE_SEND_QUOTA_OVERFLOW, + drop_channel.drop_code) + + def test_invalid_encapsulated_message(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + + first_byte = (mux._MUX_OPCODE_ADD_CHANNEL_REQUEST << 5) + block = (chr(first_byte) + + mux._encode_channel_id(1) + + mux._encode_number(0)) + payload = mux._encode_channel_id(mux._CONTROL_CHANNEL_ID) + block + text_frame = create_binary_frame(payload, opcode=common.OPCODE_TEXT, + mask=True) + request.connection.put_bytes(text_frame) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + drop_channel = next( + b for b in request.connection.get_written_control_blocks() + if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL) + self.assertEqual(mux._DROP_CODE_INVALID_ENCAPSULATING_MESSAGE, + drop_channel.drop_code) + self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR, + request.connection.server_close_code) + + def test_channel_id_truncated(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + + # The last byte of the channel id is missing. + frame = create_binary_frame('\x80', mask=True) + request.connection.put_bytes(frame) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + drop_channel = next( + b for b in request.connection.get_written_control_blocks() + if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL) + self.assertEqual(mux._DROP_CODE_CHANNEL_ID_TRUNCATED, + drop_channel.drop_code) + self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR, + request.connection.server_close_code) + + def test_inner_frame_truncated(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + + # Just contain channel id 1. + frame = create_binary_frame('\x01', mask=True) + request.connection.put_bytes(frame) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + drop_channel = next( + b for b in request.connection.get_written_control_blocks() + if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL) + self.assertEqual(mux._DROP_CODE_ENCAPSULATED_FRAME_IS_TRUNCATED, + drop_channel.drop_code) + self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR, + request.connection.server_close_code) + + def test_unknown_mux_opcode(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + + # Undefined opcode 5 + frame = create_binary_frame('\x00\xa0', mask=True) + request.connection.put_bytes(frame) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + drop_channel = next( + b for b in request.connection.get_written_control_blocks() + if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL) + self.assertEqual(mux._DROP_CODE_UNKNOWN_MUX_OPCODE, + drop_channel.drop_code) + self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR, + request.connection.server_close_code) + + def test_invalid_mux_control_block(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + + # DropChannel contains 1 byte reason + frame = create_binary_frame('\x00\x60\x00\x01\x00', mask=True) + request.connection.put_bytes(frame) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + drop_channel = next( + b for b in request.connection.get_written_control_blocks() + if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL) + self.assertEqual(mux._DROP_CODE_INVALID_MUX_CONTROL_BLOCK, + drop_channel.drop_code) + self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR, + request.connection.server_close_code) + + def test_permessage_compress(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + # Enable permessage compress extension on logical channel 2. + extensions = '%s; method=deflate' % ( + common.PERMESSAGE_COMPRESSION_EXTENSION) + encoded_handshake = _create_request_header(path='/echo', + extensions=extensions) + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + flow_control = _create_flow_control_frame(channel_id=2, + replenished_quota=20) + request.connection.put_bytes(flow_control) + + # Send compressed 'Hello' twice. + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + compressed_hello1 = compress.compress('Hello') + compressed_hello1 += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello1 = compressed_hello1[:-4] + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message=compressed_hello1, + rsv1=True)) + compressed_hello2 = compress.compress('Hello') + compressed_hello2 += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello2 = compressed_hello2[:-4] + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message=compressed_hello2, + rsv1=True)) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + request.connection.put_bytes( + _create_logical_frame(channel_id=2, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + self.assertEqual(['Hello', 'Hello'], + dispatcher.channel_events[2].messages) + # Written 'Hello's should be compressed. + messages = request.connection.get_written_messages(2) + self.assertEqual(2, len(messages)) + self.assertEqual(compressed_hello1, messages[0]) + self.assertEqual(compressed_hello2, messages[1]) + + + def test_permessage_compress_fragmented_message(self): + extensions = common.parse_extensions( + '%s; method=deflate' % common.PERMESSAGE_COMPRESSION_EXTENSION) + request = _create_mock_request( + logical_channel_extensions=extensions) + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + # Send compressed 'HelloHelloHello' as fragmented message. + compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + compressed_hello = compress.compress('HelloHelloHello') + compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH) + compressed_hello = compressed_hello[:-4] + + m = len(compressed_hello) / 2 + request.connection.put_bytes( + _create_logical_frame(channel_id=1, + message=compressed_hello[:m], + fin=False, rsv1=True, + opcode=common.OPCODE_TEXT)) + request.connection.put_bytes( + _create_logical_frame(channel_id=1, + message=compressed_hello[m:], + fin=True, rsv1=False, + opcode=common.OPCODE_CONTINUATION)) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + self.assertEqual(['HelloHelloHello'], + dispatcher.channel_events[1].messages) + messages = request.connection.get_written_messages(1) + self.assertEqual(1, len(messages)) + self.assertEqual(compressed_hello, messages[0]) + + def test_receive_bad_fragmented_message(self): + request = _create_mock_request() + dispatcher = _MuxMockDispatcher() + mux_handler = mux._MuxHandler(request, dispatcher) + mux_handler.start() + mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, + mux._INITIAL_QUOTA_FOR_CLIENT) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=2, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + # Send a frame with fin=False, and then send a frame with + # opcode=TEXT (not CONTINUATION). Logical channel 2 should be dropped. + frame1 = _create_logical_frame(channel_id=2, + message='Hello ', + fin=False, + opcode=common.OPCODE_TEXT) + request.connection.put_bytes(frame1) + frame2 = _create_logical_frame(channel_id=2, + message='World!', + fin=True, + opcode=common.OPCODE_TEXT) + request.connection.put_bytes(frame2) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=3, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + # Send a frame with opcode=CONTINUATION without a preceding frame + # the fin of which is not set. Logical channel 3 should be dropped. + frame3 = _create_logical_frame(channel_id=3, + message='Hello', + fin=True, + opcode=common.OPCODE_CONTINUATION) + request.connection.put_bytes(frame3) + + encoded_handshake = _create_request_header(path='/echo') + add_channel_request = _create_add_channel_request_frame( + channel_id=4, encoding=0, + encoded_handshake=encoded_handshake) + request.connection.put_bytes(add_channel_request) + + # Send a frame with opcode=PING and fin=False, and then send a frame + # with opcode=TEXT (not CONTINUATION). Logical channel 4 should be + # dropped. + frame4 = _create_logical_frame(channel_id=4, + message='Ping', + fin=False, + opcode=common.OPCODE_PING) + request.connection.put_bytes(frame4) + frame5 = _create_logical_frame(channel_id=4, + message='Hello', + fin=True, + opcode=common.OPCODE_TEXT) + request.connection.put_bytes(frame5) + + request.connection.put_bytes( + _create_logical_frame(channel_id=1, message='Goodbye')) + + self.assertTrue(mux_handler.wait_until_done(timeout=2)) + + drop_channels = [ + b for b in request.connection.get_written_control_blocks() + if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL] + self.assertEqual(3, len(drop_channels)) + for d in drop_channels: + self.assertEqual(mux._DROP_CODE_BAD_FRAGMENTATION, + d.drop_code) + + +if __name__ == '__main__': + unittest.main() + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream.py new file mode 100755 index 000000000..81acfeb04 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Tests for stream module.""" + + +import unittest + +import set_sys_path # Update sys.path to locate mod_pywebsocket module. + +from mod_pywebsocket import common +from mod_pywebsocket import stream + + +class StreamTest(unittest.TestCase): + """A unittest for stream module.""" + + def test_create_header(self): + # more, rsv1, ..., rsv4 are all true + header = stream.create_header(common.OPCODE_TEXT, 1, 1, 1, 1, 1, 1) + self.assertEqual('\xf1\x81', header) + + # Maximum payload size + header = stream.create_header( + common.OPCODE_TEXT, (1 << 63) - 1, 0, 0, 0, 0, 0) + self.assertEqual('\x01\x7f\x7f\xff\xff\xff\xff\xff\xff\xff', header) + + # Invalid opcode 0x10 + self.assertRaises(ValueError, + stream.create_header, + 0x10, 0, 0, 0, 0, 0, 0) + + # Invalid value 0xf passed to more parameter + self.assertRaises(ValueError, + stream.create_header, + common.OPCODE_TEXT, 0, 0xf, 0, 0, 0, 0) + + # Too long payload_length + self.assertRaises(ValueError, + stream.create_header, + common.OPCODE_TEXT, 1 << 63, 0, 0, 0, 0, 0) + + +if __name__ == '__main__': + unittest.main() + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream_hixie75.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream_hixie75.py new file mode 100755 index 000000000..ca9ac7130 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream_hixie75.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Tests for stream module.""" + + +import unittest + +import set_sys_path # Update sys.path to locate mod_pywebsocket module. + +from mod_pywebsocket.stream import StreamHixie75 +from test.test_msgutil import _create_request_hixie75 + + +class StreamHixie75Test(unittest.TestCase): + """A unittest for StreamHixie75 class.""" + + def test_payload_length(self): + for length, bytes in ((0, '\x00'), (0x7f, '\x7f'), (0x80, '\x81\x00'), + (0x1234, '\x80\xa4\x34')): + test_stream = StreamHixie75(_create_request_hixie75(bytes)) + self.assertEqual( + length, test_stream._read_payload_length_hixie75()) + + +if __name__ == '__main__': + unittest.main() + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_util.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_util.py new file mode 100755 index 000000000..20f4ab059 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_util.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python +# +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Tests for util module.""" + + +import os +import random +import sys +import unittest + +import set_sys_path # Update sys.path to locate mod_pywebsocket module. + +from mod_pywebsocket import util + + +_TEST_DATA_DIR = os.path.join(os.path.split(__file__)[0], 'testdata') + + +class UtilTest(unittest.TestCase): + """A unittest for util module.""" + + def test_get_stack_trace(self): + self.assertEqual('None\n', util.get_stack_trace()) + try: + a = 1 / 0 # Intentionally raise exception. + except Exception: + trace = util.get_stack_trace() + self.failUnless(trace.startswith('Traceback')) + self.failUnless(trace.find('ZeroDivisionError') != -1) + + def test_prepend_message_to_exception(self): + exc = Exception('World') + self.assertEqual('World', str(exc)) + util.prepend_message_to_exception('Hello ', exc) + self.assertEqual('Hello World', str(exc)) + + def test_get_script_interp(self): + cygwin_path = 'c:\\cygwin\\bin' + cygwin_perl = os.path.join(cygwin_path, 'perl') + self.assertEqual(None, util.get_script_interp( + os.path.join(_TEST_DATA_DIR, 'README'))) + self.assertEqual(None, util.get_script_interp( + os.path.join(_TEST_DATA_DIR, 'README'), cygwin_path)) + self.assertEqual('/usr/bin/perl -wT', util.get_script_interp( + os.path.join(_TEST_DATA_DIR, 'hello.pl'))) + self.assertEqual(cygwin_perl + ' -wT', util.get_script_interp( + os.path.join(_TEST_DATA_DIR, 'hello.pl'), cygwin_path)) + + def test_hexify(self): + self.assertEqual('61 7a 41 5a 30 39 20 09 0d 0a 00 ff', + util.hexify('azAZ09 \t\r\n\x00\xff')) + + +class RepeatedXorMaskerTest(unittest.TestCase): + """A unittest for RepeatedXorMasker class.""" + + def test_mask(self): + # Sample input e6,97,a5 is U+65e5 in UTF-8 + masker = util.RepeatedXorMasker('\xff\xff\xff\xff') + result = masker.mask('\xe6\x97\xa5') + self.assertEqual('\x19\x68\x5a', result) + + masker = util.RepeatedXorMasker('\x00\x00\x00\x00') + result = masker.mask('\xe6\x97\xa5') + self.assertEqual('\xe6\x97\xa5', result) + + masker = util.RepeatedXorMasker('\xe6\x97\xa5\x20') + result = masker.mask('\xe6\x97\xa5') + self.assertEqual('\x00\x00\x00', result) + + def test_mask_twice(self): + masker = util.RepeatedXorMasker('\x00\x7f\xff\x20') + # mask[0], mask[1], ... will be used. + result = masker.mask('\x00\x00\x00\x00\x00') + self.assertEqual('\x00\x7f\xff\x20\x00', result) + # mask[2], mask[0], ... will be used for the next call. + result = masker.mask('\x00\x00\x00\x00\x00') + self.assertEqual('\x7f\xff\x20\x00\x7f', result) + + def test_mask_large_data(self): + masker = util.RepeatedXorMasker('mASk') + original = ''.join([chr(i % 256) for i in xrange(1000)]) + result = masker.mask(original) + expected = ''.join( + [chr((i % 256) ^ ord('mASk'[i % 4])) for i in xrange(1000)]) + self.assertEqual(expected, result) + + masker = util.RepeatedXorMasker('MaSk') + first_part = 'The WebSocket Protocol enables two-way communication.' + result = masker.mask(first_part) + self.assertEqual( + '\x19\t6K\x1a\x0418"\x028\x0e9A\x03\x19"\x15<\x08"\rs\x0e#' + '\x001\x07(\x12s\x1f:\x0e~\x1c,\x18s\x08"\x0c>\x1e#\x080\n9' + '\x08<\x05c', + result) + second_part = 'It has two parts: a handshake and the data transfer.' + result = masker.mask(second_part) + self.assertEqual( + "('K%\x00 K9\x16<K=\x00!\x1f>[s\nm\t2\x05)\x12;\n&\x04s\n#" + "\x05s\x1f%\x04s\x0f,\x152K9\x132\x05>\x076\x19c", + result) + + +def get_random_section(source, min_num_chunks): + chunks = [] + bytes_chunked = 0 + + while bytes_chunked < len(source): + chunk_size = random.randint( + 1, + min(len(source) / min_num_chunks, len(source) - bytes_chunked)) + chunk = source[bytes_chunked:bytes_chunked + chunk_size] + chunks.append(chunk) + bytes_chunked += chunk_size + + return chunks + + +class InflaterDeflaterTest(unittest.TestCase): + """A unittest for _Inflater and _Deflater class.""" + + def test_inflate_deflate_default(self): + input = b'hello' + '-' * 30000 + b'hello' + inflater15 = util._Inflater(15) + deflater15 = util._Deflater(15) + inflater8 = util._Inflater(8) + deflater8 = util._Deflater(8) + + compressed15 = deflater15.compress_and_finish(input) + compressed8 = deflater8.compress_and_finish(input) + + inflater15.append(compressed15) + inflater8.append(compressed8) + + self.assertNotEqual(compressed15, compressed8) + self.assertEqual(input, inflater15.decompress(-1)) + self.assertEqual(input, inflater8.decompress(-1)) + + def test_random_section(self): + random.seed(a=0) + source = ''.join( + [chr(random.randint(0, 255)) for i in xrange(100 * 1024)]) + + chunked_input = get_random_section(source, 10) + print "Input chunk sizes: %r" % [len(c) for c in chunked_input] + + deflater = util._Deflater(15) + compressed = [] + for chunk in chunked_input: + compressed.append(deflater.compress(chunk)) + compressed.append(deflater.compress_and_finish('')) + + chunked_expectation = get_random_section(source, 10) + print ("Expectation chunk sizes: %r" % + [len(c) for c in chunked_expectation]) + + inflater = util._Inflater(15) + inflater.append(''.join(compressed)) + for chunk in chunked_expectation: + decompressed = inflater.decompress(len(chunk)) + self.assertEqual(chunk, decompressed) + + self.assertEqual('', inflater.decompress(-1)) + + +if __name__ == '__main__': + unittest.main() + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/README b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/README new file mode 100644 index 000000000..c001aa559 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/README @@ -0,0 +1 @@ +Test data directory diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/abort_by_user_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/abort_by_user_wsh.py new file mode 100644 index 000000000..367f9930f --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/abort_by_user_wsh.py @@ -0,0 +1,42 @@ +# Copyright 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +from mod_pywebsocket import handshake + + +def web_socket_do_extra_handshake(request): + raise handshake.AbortedByUserException("abort for test") + + +def web_socket_transfer_data(request): + raise handshake.AbortedByUserException("abort for test") + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/blank_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/blank_wsh.py new file mode 100644 index 000000000..7f87c6af2 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/blank_wsh.py @@ -0,0 +1,31 @@ +# Copyright 2009, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +# intentionally left blank diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/origin_check_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/origin_check_wsh.py new file mode 100644 index 000000000..2c139fa17 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/origin_check_wsh.py @@ -0,0 +1,42 @@ +# Copyright 2009, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +def web_socket_do_extra_handshake(request): + if request.ws_origin == 'http://example.com': + return + raise ValueError('Unacceptable origin: %r' % request.ws_origin) + + +def web_socket_transfer_data(request): + request.connection.write('origin_check_wsh.py is called for %s, %s' % + (request.ws_resource, request.ws_protocol)) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/exception_in_transfer_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/exception_in_transfer_wsh.py new file mode 100644 index 000000000..b982d0231 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/exception_in_transfer_wsh.py @@ -0,0 +1,44 @@ +# Copyright 2009, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Exception in web_socket_transfer_data(). +""" + + +def web_socket_do_extra_handshake(request): + pass + + +def web_socket_transfer_data(request): + raise Exception('Intentional Exception for %s, %s' % + (request.ws_resource, request.ws_protocol)) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/no_wsh_at_the_end.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/no_wsh_at_the_end.py new file mode 100644 index 000000000..17e7be180 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/no_wsh_at_the_end.py @@ -0,0 +1,45 @@ +# Copyright 2009, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Correct signatures, wrong file name. +""" + + +def web_socket_do_extra_handshake(request): + pass + + +def web_socket_transfer_data(request): + request.connection.write( + 'sub/no_wsh_at_the_end.py is called for %s, %s' % + (request.ws_resource, request.ws_protocol)) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/non_callable_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/non_callable_wsh.py new file mode 100644 index 000000000..26352eb4c --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/non_callable_wsh.py @@ -0,0 +1,39 @@ +# Copyright 2009, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Non-callable handlers. +""" + + +web_socket_do_extra_handshake = True +web_socket_transfer_data = 1 + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/plain_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/plain_wsh.py new file mode 100644 index 000000000..db3ff6930 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/plain_wsh.py @@ -0,0 +1,40 @@ +# Copyright 2009, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +def web_socket_do_extra_handshake(request): + pass + + +def web_socket_transfer_data(request): + request.connection.write('sub/plain_wsh.py is called for %s, %s' % + (request.ws_resource, request.ws_protocol)) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py new file mode 100644 index 000000000..6bf659bc9 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py @@ -0,0 +1,45 @@ +# Copyright 2009, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Wrong web_socket_do_extra_handshake signature. +""" + + +def no_web_socket_do_extra_handshake(request): + pass + + +def web_socket_transfer_data(request): + request.connection.write( + 'sub/wrong_handshake_sig_wsh.py is called for %s, %s' % + (request.ws_resource, request.ws_protocol)) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py new file mode 100644 index 000000000..e0e2e5507 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py @@ -0,0 +1,45 @@ +# Copyright 2009, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Wrong web_socket_transfer_data() signature. +""" + + +def web_socket_do_extra_handshake(request): + pass + + +def no_web_socket_transfer_data(request): + request.connection.write( + 'sub/wrong_transfer_sig_wsh.py is called for %s, %s' % + (request.ws_resource, request.ws_protocol)) + + +# vi:sts=4 sw=4 et diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/hello.pl b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/hello.pl new file mode 100644 index 000000000..882ef5a10 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/hello.pl @@ -0,0 +1,32 @@ +#!/usr/bin/perl -wT +# +# Copyright 2012, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +print "Hello\n"; |