Commit 0665b8d7 authored by yhirano@chromium.org's avatar yhirano@chromium.org

[WebSocket] Add tests for permessage deflate split frames.

Upgrade pywebsocket to r804.

BUG=NONE
R=tyoshino

Review URL: https://codereview.chromium.org/314043002

git-svn-id: svn://svn.chromium.org/blink/trunk@175544 bbb929c8-8fbe-4397-9dbb-9b2b20218538
parent 23fb6337
Deflated message is split to multiple frames.
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
onmessage: HelloHello
onclose
PASS successfullyParsed is true
TEST COMPLETE
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<script src="/js-test-resources/js-test.js"></script>
</head>
<body>
<script type="text/javascript">
description("Deflated message is split to multiple frames.");
window.jsTestIsAsync = true;
var ws = new WebSocket('ws://localhost:8880/permessage-deflate-split-frames');
ws.onopen = function()
{
ws.send('kick');
};
ws.onmessage = function(e)
{
debug('onmessage: ' + e.data);
}
ws.onclose = function(e)
{
debug('onclose');
finishJSTest();
};
</script>
</body>
</html>
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from mod_pywebsocket import common
from mod_pywebsocket.extensions import PerMessageDeflateExtensionProcessor
from mod_pywebsocket.stream import create_header
def _get_permessage_deflate_extension_processor(request):
for extension_processor in request.ws_extension_processors:
if isinstance(extension_processor,
PerMessageDeflateExtensionProcessor):
return extension_processor
return None
def web_socket_do_extra_handshake(request):
processor = _get_permessage_deflate_extension_processor(request)
assert processor is not None
# Remove extension processors other than
# PerMessageDeflateExtensionProcessor to avoid conflict.
request.ws_extension_processors = [processor]
def web_socket_transfer_data(request):
line = request.ws_stream.receive_message()
# Hello
payload = b'\xf2\x48\xcd\xc9\xc9\x07\x00\x00\x00\xff\xff'
# Strip \x00\x00\xff\xff
stripped = payload[:-4]
header = create_header(common.OPCODE_TEXT, len(payload),
fin=0, rsv1=1, rsv2=0, rsv3=0, mask=False)
request.ws_stream._write(header + payload)
header = create_header(common.OPCODE_CONTINUATION, len(stripped),
fin=1, rsv1=0, rsv2=0, rsv3=0, mask=False)
request.ws_stream._write(header + stripped)
# vi:sts=4 sw=4 et
......@@ -292,6 +292,31 @@ TEST(WebSocketPerMessageDeflateTest, TestInflate)
EXPECT_TRUE(f3.final);
}
TEST(WebSocketPerMessageDeflateTest, TestInflateMultipleBlocksOverMultipleFrames)
{
WebSocketPerMessageDeflate c;
c.enable(8, WebSocketDeflater::TakeOverContext);
WebSocketFrame::OpCode opcode = WebSocketFrame::OpCodeText;
WebSocketFrame::OpCode continuation = WebSocketFrame::OpCodeContinuation;
std::string expected = "HelloHello";
std::string actual;
WebSocketFrame f1(opcode, "\xf2\x48\xcd\xc9\xc9\x07\x00\x00\x00\xff\xff", 11, WebSocketFrame::Compress);
WebSocketFrame f2(continuation, "\xf2\x00\x11\x00\x00", 5, WebSocketFrame::Final);
ASSERT_TRUE(c.inflate(f1));
EXPECT_FALSE(f1.compress);
EXPECT_FALSE(f1.final);
actual += std::string(f1.payload, f1.payloadLength);
c.resetInflateBuffer();
ASSERT_TRUE(c.inflate(f2));
EXPECT_FALSE(f2.compress);
EXPECT_TRUE(f2.final);
actual += std::string(f2.payload, f2.payloadLength);
EXPECT_EQ(expected, actual);
}
TEST(WebSocketPerMessageDeflateTest, TestInflateEmptyFrame)
{
WebSocketPerMessageDeflate c;
......
......@@ -102,7 +102,6 @@ SEC_WEBSOCKET_LOCATION_HEADER = 'Sec-WebSocket-Location'
# Extensions
DEFLATE_FRAME_EXTENSION = 'deflate-frame'
PERFRAME_COMPRESSION_EXTENSION = 'perframe-compress'
PERMESSAGE_COMPRESSION_EXTENSION = 'permessage-compress'
PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate'
X_WEBKIT_DEFLATE_FRAME_EXTENSION = 'x-webkit-deflate-frame'
......@@ -195,7 +194,7 @@ class ExtensionParsingException(Exception):
super(ExtensionParsingException, self).__init__(name)
def _parse_extension_param(state, definition, allow_quoted_string):
def _parse_extension_param(state, definition):
param_name = http_header_util.consume_token(state)
if param_name is None:
......@@ -209,11 +208,8 @@ def _parse_extension_param(state, definition, allow_quoted_string):
http_header_util.consume_lwses(state)
if allow_quoted_string:
# TODO(toyoshim): Add code to validate that parsed param_value is token
param_value = http_header_util.consume_token_or_quoted_string(state)
else:
param_value = http_header_util.consume_token(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 '
......@@ -222,7 +218,7 @@ def _parse_extension_param(state, definition, allow_quoted_string):
definition.add_parameter(param_name, param_value)
def _parse_extension(state, allow_quoted_string):
def _parse_extension(state):
extension_token = http_header_util.consume_token(state)
if extension_token is None:
return None
......@@ -238,7 +234,7 @@ def _parse_extension(state, allow_quoted_string):
http_header_util.consume_lwses(state)
try:
_parse_extension_param(state, extension, allow_quoted_string)
_parse_extension_param(state, extension)
except ExtensionParsingException, e:
raise ExtensionParsingException(
'Failed to parse parameter for %r (%r)' %
......@@ -247,7 +243,7 @@ def _parse_extension(state, allow_quoted_string):
return extension
def parse_extensions(data, allow_quoted_string=False):
def parse_extensions(data):
"""Parses Sec-WebSocket-Extensions header value returns a list of
ExtensionParameter objects.
......@@ -258,7 +254,7 @@ def parse_extensions(data, allow_quoted_string=False):
extension_list = []
while True:
extension = _parse_extension(state, allow_quoted_string)
extension = _parse_extension(state)
if extension is not None:
extension_list.append(extension)
......
......@@ -330,7 +330,7 @@ _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, allow_quoted_string=True)
return common.parse_extensions(data)
def _create_accepted_method_desc(method_name, method_params):
......@@ -421,32 +421,6 @@ class CompressionExtensionProcessorBase(ExtensionProcessorInterface):
return self._compression_processor
class PerFrameCompressExtensionProcessor(CompressionExtensionProcessorBase):
"""perframe-compress processor.
Specification:
http://tools.ietf.org/html/draft-ietf-hybi-websocket-perframe-compression
"""
_DEFLATE_METHOD = 'deflate'
def __init__(self, request):
CompressionExtensionProcessorBase.__init__(self, request)
def name(self):
return common.PERFRAME_COMPRESSION_EXTENSION
def _lookup_compression_processor(self, method_desc):
if method_desc.name() == self._DEFLATE_METHOD:
return DeflateFrameExtensionProcessor(method_desc)
return None
_available_processors[common.PERFRAME_COMPRESSION_EXTENSION] = (
PerFrameCompressExtensionProcessor)
_compression_extension_names.append(common.PERFRAME_COMPRESSION_EXTENSION)
class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface):
"""permessage-deflate extension processor. It's also used for
permessage-compress extension when the deflate method is chosen.
......@@ -690,7 +664,7 @@ class _PerMessageDeflateFramer(object):
original_payload_size)
message = self._rfc1979_deflater.filter(
message, flush=end, bfinal=self._bfinal)
message, end=end, bfinal=self._bfinal)
filtered_payload_size = len(message)
self._outgoing_average_ratio_calculator.add_result_bytes(
......@@ -844,16 +818,14 @@ class MuxExtensionProcessor(ExtensionProcessorInterface):
# 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.PERFRAME_COMPRESSION_EXTENSION or
name == common.DEFLATE_FRAME_EXTENSION or
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.PERFRAME_COMPRESSION_EXTENSION or
name == common.DEFLATE_FRAME_EXTENSION or
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):
......
......@@ -69,13 +69,10 @@ from mod_pywebsocket import util
_SEC_WEBSOCKET_KEY_REGEX = re.compile('^[+/0-9A-Za-z]{21}[AQgw]==$')
# Defining aliases for values used frequently.
_VERSION_HYBI08 = common.VERSION_HYBI08
_VERSION_HYBI08_STRING = str(_VERSION_HYBI08)
_VERSION_LATEST = common.VERSION_HYBI_LATEST
_VERSION_LATEST_STRING = str(_VERSION_LATEST)
_SUPPORTED_VERSIONS = [
_VERSION_LATEST,
_VERSION_HYBI08,
]
......@@ -150,9 +147,6 @@ class Handshaker(object):
self._request.ws_version = self._check_version()
# This handshake must be based on latest hybi. We are responsible to
# fallback to HTTP on handshake failure as latest hybi handshake
# specifies.
try:
self._get_origin()
self._set_protocol()
......@@ -286,10 +280,7 @@ class Handshaker(object):
raise e
def _get_origin(self):
if self._request.ws_version is _VERSION_HYBI08:
origin_header = common.SEC_WEBSOCKET_ORIGIN_HEADER
else:
origin_header = common.ORIGIN_HEADER
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')
......@@ -298,8 +289,6 @@ class Handshaker(object):
def _check_version(self):
version = get_mandatory_header(self._request,
common.SEC_WEBSOCKET_VERSION_HEADER)
if version == _VERSION_HYBI08_STRING:
return _VERSION_HYBI08
if version == _VERSION_LATEST_STRING:
return _VERSION_LATEST
......@@ -335,13 +324,9 @@ class Handshaker(object):
self._request.ws_requested_extensions = None
return
if self._request.ws_version is common.VERSION_HYBI08:
allow_quoted_string=False
else:
allow_quoted_string=True
try:
self._request.ws_requested_extensions = common.parse_extensions(
extensions_header, allow_quoted_string=allow_quoted_string)
extensions_header)
except common.ExtensionParsingException, e:
raise HandshakeException(
'Failed to parse Sec-WebSocket-Extensions header: %r' % e)
......
......@@ -36,6 +36,7 @@ Use this file to launch pywebsocket without Apache HTTP Server.
BASIC USAGE
===========
Go to the src directory and run
......@@ -61,10 +62,13 @@ For trouble shooting, adding "--log_level debug" might help you.
TRY DEMO
========
Go to the src directory and run
Go to the src directory and run standalone.py with -d option to set the
document root to the directory containing example HTMLs and handlers like this:
$ python standalone.py -d example
$ cd src
$ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example
to launch pywebsocket with the sample handler and html on port 80. Open
http://localhost/console.html, click the connect button, type something into
......@@ -72,24 +76,48 @@ the text box next to the send button and click the send button. If everything
is working, you'll see the message you typed echoed by the server.
SUPPORTING TLS
USING TLS
=========
To support TLS, run standalone.py with -t, -k, and -c options.
To run the standalone server with TLS support, run it with -t, -k, and -c
options. When TLS is enabled, the standalone server accepts only TLS connection.
Note that when ssl module is used and the key/cert location is incorrect,
TLS connection silently fails while pyOpenSSL fails on startup.
Example:
$ PYTHONPATH=. python mod_pywebsocket/standalone.py \
-d example \
-p 10443 \
-t \
-c ../test/cert/cert.pem \
-k ../test/cert/key.pem \
Note that when passing a relative path to -c and -k option, it will be resolved
using the document root directory as the base.
SUPPORTING CLIENT AUTHENTICATION
USING CLIENT AUTHENTICATION
===========================
To support client authentication with TLS, run standalone.py with -t, -k, -c,
and --tls-client-auth, and --tls-client-ca options.
To run the standalone server with TLS client authentication support, run it with
--tls-client-auth and --tls-client-ca options in addition to ones required for
TLS support.
E.g., $./standalone.py -d ../example -p 10443 -t -c ../test/cert/cert.pem -k
../test/cert/key.pem --tls-client-auth --tls-client-ca=../test/cert/cacert.pem
Example:
$ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example -p 10443 -t \
-c ../test/cert/cert.pem -k ../test/cert/key.pem \
--tls-client-auth \
--tls-client-ca=../test/cert/cacert.pem
Note that when passing a relative path to --tls-client-ca option, it will be
resolved using the document root directory as the base.
CONFIGURATION FILE
==================
You can also write a configuration file and use it by specifying the path to
the configuration file by --config option. Please write a configuration file
......@@ -113,12 +141,14 @@ configuration file.
THREADING
=========
This server is derived from SocketServer.ThreadingMixIn. Hence a thread is
used for each request.
SECURITY WARNING
================
This uses CGIHTTPServer and CGIHTTPServer is not secure.
It may execute arbitrary Python code or external programs. It should not be
......@@ -149,6 +179,7 @@ from mod_pywebsocket import handshake
from mod_pywebsocket import http_header_util
from mod_pywebsocket import memorizingfile
from mod_pywebsocket import util
from mod_pywebsocket.xhr_benchmark_handler import XHRBenchmarkHandler
_DEFAULT_LOG_MAX_BYTES = 1024 * 256
......@@ -662,99 +693,6 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
CGIHTTPServer.CGIHTTPRequestHandler.__init__(
self, request, client_address, server)
def _xhr_send_benchmark_helper(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)
for c in received_data:
if c != 'a':
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 _xhr_receive_benchmark_helper(self):
content_length = self.headers.getheader('Content-Length')
request_body = self.rfile.read(int(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()
def parse_request(self):
"""Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request.
......@@ -791,10 +729,14 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
# Special paths for XMLHttpRequest benchmark
xhr_benchmark_helper_prefix = '/073be001e10950692ccbf3a2ad21c245'
if resource == (xhr_benchmark_helper_prefix + '_send'):
self._xhr_send_benchmark_helper()
xhr_benchmark_handler = XHRBenchmarkHandler(
self.headers, self.rfile, self.wfile)
xhr_benchmark_handler.do_send()
return False
if resource == (xhr_benchmark_helper_prefix + '_receive'):
self._xhr_receive_benchmark_helper()
xhr_benchmark_handler = XHRBenchmarkHandler(
self.headers, self.rfile, self.wfile)
xhr_benchmark_handler.do_receive()
return False
if resource is None:
......
......@@ -331,8 +331,8 @@ class _RFC1979Deflater(object):
self._window_bits = window_bits
self._no_context_takeover = no_context_takeover
def filter(self, bytes, flush=True, bfinal=False):
if self._deflater is None or (self._no_context_takeover and flush):
def filter(self, bytes, end=True, bfinal=False):
if self._deflater is None:
self._deflater = _Deflater(self._window_bits)
if bfinal:
......@@ -341,11 +341,17 @@ class _RFC1979Deflater(object):
result = result + chr(0)
self._deflater = None
return result
if flush:
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.
return self._deflater.compress_and_flush(bytes)[:-4]
return self._deflater.compress(bytes)
result = result[:-4]
if self._no_context_takeover and end:
self._deflater = None
return result
class _RFC1979Inflater(object):
......
# 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)
for c in received_data:
if c != 'a':
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()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment