mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-07 08:18:41 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
28
testing/web-platform/tests/tools/pywebsocket/src/COPYING
Normal file
28
testing/web-platform/tests/tools/pywebsocket/src/COPYING
Normal file
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
include COPYING
|
||||
include MANIFEST.in
|
||||
include README
|
||||
recursive-include example *.py
|
||||
recursive-include mod_pywebsocket *.py
|
||||
recursive-include test *.py
|
||||
17
testing/web-platform/tests/tools/pywebsocket/src/README
Normal file
17
testing/web-platform/tests/tools/pywebsocket/src/README
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>"""
|
||||
177
testing/web-platform/tests/tools/pywebsocket/src/example/util.js
Normal file
177
testing/web-platform/tests/tools/pywebsocket/src/example/util.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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});
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
%}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
74
testing/web-platform/tests/tools/pywebsocket/src/setup.py
Normal file
74
testing/web-platform/tests/tools/pywebsocket/src/setup.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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-----
|
||||
|
|
@ -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-----
|
||||
Binary file not shown.
|
|
@ -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-----
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
221
testing/web-platform/tests/tools/pywebsocket/src/test/mock.py
Normal file
221
testing/web-platform/tests/tools/pywebsocket/src/test/mock.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
File diff suppressed because it is too large
Load diff
2089
testing/web-platform/tests/tools/pywebsocket/src/test/test_mux.py
Normal file
2089
testing/web-platform/tests/tools/pywebsocket/src/test/test_mux.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
1
testing/web-platform/tests/tools/pywebsocket/src/test/testdata/README
vendored
Normal file
1
testing/web-platform/tests/tools/pywebsocket/src/test/testdata/README
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
Test data directory
|
||||
42
testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/abort_by_user_wsh.py
vendored
Normal file
42
testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/abort_by_user_wsh.py
vendored
Normal file
|
|
@ -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
|
||||
31
testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/blank_wsh.py
vendored
Normal file
31
testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/blank_wsh.py
vendored
Normal file
|
|
@ -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
|
||||
42
testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/origin_check_wsh.py
vendored
Normal file
42
testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/origin_check_wsh.py
vendored
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
45
testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/no_wsh_at_the_end.py
vendored
Normal file
45
testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/no_wsh_at_the_end.py
vendored
Normal file
|
|
@ -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
|
||||
39
testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/non_callable_wsh.py
vendored
Normal file
39
testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/non_callable_wsh.py
vendored
Normal file
|
|
@ -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
|
||||
40
testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/plain_wsh.py
vendored
Normal file
40
testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/plain_wsh.py
vendored
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
32
testing/web-platform/tests/tools/pywebsocket/src/test/testdata/hello.pl
vendored
Normal file
32
testing/web-platform/tests/tools/pywebsocket/src/test/testdata/hello.pl
vendored
Normal file
|
|
@ -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";
|
||||
Loading…
Add table
Add a link
Reference in a new issue