Bug 689006: Upgrade pywebsocket to binary API (version 553, also supports Python > 2.5) r=mcmanus

This commit is contained in:
Jason Duell 2011-12-15 15:21:55 -08:00
parent df88daa742
commit 7ed95dce7f
22 changed files with 1414 additions and 568 deletions

View File

@ -108,6 +108,7 @@ _MOD_PYWEBSOCKET_FILES = \
pywebsocket/mod_pywebsocket/__init__.py \
pywebsocket/mod_pywebsocket/common.py \
pywebsocket/mod_pywebsocket/dispatch.py \
pywebsocket/mod_pywebsocket/extensions.py \
pywebsocket/mod_pywebsocket/headerparserhandler.py \
pywebsocket/mod_pywebsocket/http_header_util.py \
pywebsocket/mod_pywebsocket/memorizingfile.py \
@ -115,7 +116,7 @@ _MOD_PYWEBSOCKET_FILES = \
pywebsocket/mod_pywebsocket/stream.py \
pywebsocket/mod_pywebsocket/_stream_hixie75.py \
pywebsocket/mod_pywebsocket/msgutil.py \
pywebsocket/mod_pywebsocket/_stream_hybi06.py \
pywebsocket/mod_pywebsocket/_stream_hybi.py \
pywebsocket/mod_pywebsocket/_stream_base.py \
$(NULL)
@ -124,7 +125,7 @@ _HANDSHAKE_FILES = \
pywebsocket/mod_pywebsocket/handshake/hybi00.py \
pywebsocket/mod_pywebsocket/handshake/_base.py \
pywebsocket/mod_pywebsocket/handshake/draft75.py \
pywebsocket/mod_pywebsocket/handshake/hybi06.py \
pywebsocket/mod_pywebsocket/handshake/hybi.py \
$(NULL)
_DEST_DIR = $(DEPTH)/_tests/$(relativesrcdir)

View File

@ -1,64 +1,9 @@
mod_pywebsocket http://pywebsocket.googlecode.com/svn
version 489
supporting ietf-07
Install this package by:
$ python setup.py build
$ sudo python setup.py install
includes the following minor patch:: (first bit supports symlinked wsh
files, the second allows python 2.5 to work)
If you're going to use this package as a normal user, run this instead:
$ python setup.py install --user
also includes patch for 663096 to drain input buffers before closing
in order to avoid RST
also updates blindly version 7 to be version 8 until upstream makes
real version 8 available
also includes changeset 491 from mod_pywebsocket repo - necessary to
enable wss:// testing
diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py b/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py
--- a/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py
+++ b/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py
@@ -60,17 +60,18 @@ def _normalize_path(path):
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)
+ # do not normalize away symlinks in mochitest
+ # path = os.path.realpath(path)
path = path.replace('\\', '/')
return path
def _create_path_to_resource_converter(base_dir):
base_dir = _normalize_path(base_dir)
base_len = len(base_dir)
diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_base.py b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_base.py
--- a/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_base.py
+++ b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_base.py
@@ -92,19 +92,17 @@ class StreamBase(object):
prepends remote address to the exception message and raise again.
Raises:
ConnectionTerminatedException: when read returns empty string.
"""
bytes = self._request.connection.read(length)
if not bytes:
- raise ConnectionTerminatedException(
- 'Receiving %d byte failed. Peer (%r) closed connection' %
- (length, (self._request.connection.remote_addr,)))
+ raise ConnectionTerminatedException('connection terminated: read failed')
return bytes
def _write(self, bytes):
"""Writes given bytes to connection. In case we catch any exception,
prepends remote address to the exception message and raise again.
"""
try:
Then read document by:
$ pydoc mod_pywebsocket

View File

@ -0,0 +1,51 @@
This pywebsocket code is mostly unchanged from the source at
svn checkout http://pywebsocket.googlecode.com/svn/trunk/ pywebsocket-read-only
The current Mozilla code is based on
svnversion: 553 (AKA pywebsocket version='0.6b1')
--------------------------------------------------------------------------------
STEPS TO UPDATE MOZILLA TO NEWER PYWEBSOCKET VERSION
--------------------------------------------------------------------------------
- Get new pywebsocket checkout from googlecode (into, for instance, 'src')
svn checkout http://pywebsocket.googlecode.com/svn/trunk/ pywebsocket-read-only
- Export a version w/o SVN files:
svn export src dist
- rsync new version into our tree, deleting files that aren't needed any more
(NOTE: this will blow away this file! hg revert it or keep a copy.)
rsync -r --delete dist/ $MOZ_SRC/testing/mochitest/pywebsocket
- Manually move the 'standalone.py' file from the mmod_pywebsocket/ directory to
the parent directory (not sure why we moved it: probably no reason)
- hg add/rm appropriate files, and add/remove them from _MOD_PYWEBSOCKET_FILES
and/or _HANDSHAKE_FILES in testing/mochitest/Makefile.am
- Edit the _normalize_path() function in dispatch.py and MAKE SURE THIS LINE IS
COMMENTED OUT:
# MOZILLA: do not normalize away symlinks in mochitest
# path = os.path.realpath(path)
- There's also some code in mod_pywebsocket/_stream_base.py that may or may not
need to change to support Python 2.5:
#raise ConnectionTerminatedException(
# 'Receiving %d byte failed. Peer (%r) closed connection' %
# (length, (self._request.connection.remote_addr,)))
raise ConnectionTerminatedException('connection terminated: read failed')
- Test and make sure the code works:
make mochitest-plain TEST_PATH=content/base/test/test_websocket.html
- If this doesn't take a look at the pywebsocket server log,
$OBJDIR/_tests/testing/mochitest/websock.log

View File

@ -1,4 +1,4 @@
# Copyright 2009, Google Inc.
# Copyright 2011, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
@ -76,9 +76,9 @@ Installation:
</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.
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.
@ -93,10 +93,11 @@ specified in the handshake is considered as if it is a file path under
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 two functions:
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.
@ -106,8 +107,8 @@ 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):
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
@ -121,16 +122,17 @@ The last two are a bit tricky.
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.
None, you must choose one subprotocol from this list and set it to
ws_protocol.
For Hixie 75 and 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
(WebSocket-Protocol for Hixie 75) 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
(or 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.
ws_protocol is set to the value given by the client in
Sec-WebSocket-Protocol (WebSocket-Protocol for Hixie 75) 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 (or 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.
web_socket_transfer_data is called after the handshake completed
successfully. A handler can receive/send messages from/to the client
@ -141,9 +143,9 @@ 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
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.
@ -157,8 +159,16 @@ web_socket_transfer_data cause connection close.
request.ws_stream.close_connection()
When you're using IETF HyBi 00 or later protocol, close_connection will wait
for closing handshake acknowledgement coming from the client. When it couldn't
receive a valid acknowledgement, raises an exception.
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
A WebSocket handler must be thread-safe if the server (Apache or
standalone.py) is configured to use threads.

View File

@ -43,10 +43,13 @@ from mod_pywebsocket import util
# Exceptions
class ConnectionTerminatedException(Exception):
"""This exception will be raised when a connection is terminated
unexpectedly.
"""
pass
@ -54,22 +57,33 @@ class InvalidFrameException(ConnectionTerminatedException):
"""This exception will be raised when we received an invalid frame we
cannot parse.
"""
pass
class BadOperationException(RuntimeError):
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(RuntimeError):
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
@ -97,6 +111,13 @@ class StreamBase(object):
bytes = self._request.connection.read(length)
if not bytes:
# MOZILLA: Patrick McManus found we needed this for Python 2.5 to
# work. Not sure which tests he meant: I found that
# content/base/test/test_websocket* all worked fine with 2.5 with
# the original Google code. JDuell
#raise ConnectionTerminatedException(
# 'Receiving %d byte failed. Peer (%r) closed connection' %
# (length, (self._request.connection.remote_addr,)))
raise ConnectionTerminatedException('connection terminated: read failed')
return bytes
@ -129,12 +150,6 @@ class StreamBase(object):
length -= len(new_bytes)
return ''.join(bytes)
def flushread(self):
try:
self._request.connection.flushread()
except:
pass
def _read_until(self, delim_char):
"""Reads bytes until we encounter delim_char. The result will not
contain delim_char.

View File

@ -64,11 +64,12 @@ class StreamHixie75(StreamBase):
self._request.client_terminated = False
self._request.server_terminated = False
def send_message(self, message, end=True):
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
@ -79,6 +80,10 @@ class StreamHixie75(StreamBase):
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')

View File

@ -28,7 +28,7 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Stream class for IETF HyBi 07 WebSocket protocol.
"""Stream class for IETF HyBi latest WebSocket protocol.
"""
@ -41,17 +41,26 @@ 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
def is_control_opcode(opcode):
return (opcode >> 3) == 1
_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.
@ -121,29 +130,61 @@ def _build_frame(header, body, mask):
return header + masking_nonce + masker.mask(body)
def create_text_frame(message, opcode=common.OPCODE_TEXT, fin=1, mask=False):
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')
header = create_header(opcode, len(encoded_message), fin, 0, 0, 0, mask)
return _build_frame(header, encoded_message, mask)
return create_binary_frame(encoded_message, opcode, fin, mask,
frame_filters)
class FragmentedTextFrameBuilder(object):
class FragmentedFrameBuilder(object):
"""A stateful class to send a message as fragments."""
def __init__(self, mask):
def __init__(self, mask, frame_filters=[]):
"""Constructs an instance."""
self._mask = mask
self._frame_filters = frame_filters
self._started = False
def build(self, message, end):
# 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, message, 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 = common.OPCODE_TEXT
opcode = frame_type
self._opcode = frame_type
if end:
self._started = False
@ -152,22 +193,31 @@ class FragmentedTextFrameBuilder(object):
self._started = True
fin = 0
return create_text_frame(message, opcode, fin, self._mask)
if binary:
return create_binary_frame(
message, opcode, fin, self._mask, self._frame_filters)
else:
return create_text_frame(
message, opcode, fin, self._mask, self._frame_filters)
def create_ping_frame(body, mask=False):
header = create_header(common.OPCODE_PING, len(body), 1, 0, 0, 0, mask)
return _build_frame(header, body, mask)
def _create_control_frame(opcode, body, mask, frame_filters):
frame = Frame(opcode=opcode, payload=body)
return _filter_and_format_frame_object(frame, mask, frame_filters)
def create_pong_frame(body, mask=False):
header = create_header(common.OPCODE_PONG, len(body), 1, 0, 0, 0, mask)
return _build_frame(header, body, mask)
def create_ping_frame(body, mask=False, frame_filters=[]):
return _create_control_frame(common.OPCODE_PING, body, mask, frame_filters)
def create_close_frame(body, mask=False):
header = create_header(common.OPCODE_CLOSE, len(body), 1, 0, 0, 0, mask)
return _build_frame(header, body, mask)
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)
class StreamOptions(object):
@ -176,7 +226,13 @@ class StreamOptions(object):
def __init__(self):
"""Constructs StreamOptions."""
self.deflate = False
# Enables deflate-stream extension.
self.deflate_stream = False
# Filters applied to frames.
self.outgoing_frame_filters = []
self.incoming_frame_filters = []
self.mask_send = False
self.unmask_receive = True
@ -197,8 +253,8 @@ class Stream(StreamBase):
self._options = options
if self._options.deflate:
self._logger.debug('Deflated stream')
if self._options.deflate_stream:
self._logger.debug('Setup filter for deflate-stream')
self._request = util.DeflateRequest(self._request)
self._request.client_terminated = False
@ -209,7 +265,8 @@ class Stream(StreamBase):
# Holds the opcode of the first fragment.
self._original_opcode = None
self._writer = FragmentedTextFrameBuilder(self._options.mask_send)
self._writer = FragmentedFrameBuilder(
self._options.mask_send, self._options.outgoing_frame_filters)
self._ping_queue = deque()
@ -266,29 +323,47 @@ class Stream(StreamBase):
return opcode, bytes, fin, rsv1, rsv2, rsv3
def send_message(self, message, end=True):
def _receive_frame_as_frame_object(self):
opcode, bytes, fin, rsv1, rsv2, rsv3 = self._receive_frame()
return Frame(fin=fin, rsv1=rsv1, rsv2=rsv2, rsv3=rsv3,
opcode=opcode, payload=bytes)
def send_message(self, message, end=True, binary=False):
"""Send message.
Args:
message: unicode string to send.
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.
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')
self._write(self._writer.build(message, end))
if binary and isinstance(message, unicode):
raise BadOperationException(
'Message for binary frame must be instance of str')
try:
self._write(self._writer.build(message, end, binary))
except ValueError, e:
raise BadOperationException(e)
def receive_message(self):
"""Receive a WebSocket frame and return its payload an unicode string.
"""Receive a WebSocket frame and return its payload as a text in
unicode or a binary in str.
Returns:
payload unicode string in a WebSocket frame. None iff received
closing handshake.
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.
@ -297,8 +372,8 @@ class Stream(StreamBase):
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.
flags, opcode we cannot handle. You can ignore this
exception and continue receiving the next frame.
"""
if self._request.client_terminated:
@ -310,15 +385,19 @@ class Stream(StreamBase):
# mp_conn.read will block if no bytes are available.
# Timeout is controlled by TimeOut directive of Apache.
opcode, bytes, fin, rsv1, rsv2, rsv3 = self._receive_frame()
if rsv1 or rsv2 or rsv3:
frame = self._receive_frame_as_frame_object()
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)' %
(rsv1, rsv2, rsv3))
(frame.rsv1, frame.rsv2, frame.rsv3))
if opcode == common.OPCODE_CONTINUATION:
if frame.opcode == common.OPCODE_CONTINUATION:
if not self._received_fragments:
if fin:
if frame.fin:
raise InvalidFrameException(
'Received a termination frame but fragmentation '
'not started')
@ -327,18 +406,18 @@ class Stream(StreamBase):
'Received an intermediate frame but '
'fragmentation not started')
if fin:
if frame.fin:
# End of fragmentation frame
self._received_fragments.append(bytes)
self._received_fragments.append(frame.payload)
message = ''.join(self._received_fragments)
self._received_fragments = []
else:
# Intermediate frame
self._received_fragments.append(bytes)
self._received_fragments.append(frame.payload)
continue
else:
if self._received_fragments:
if fin:
if frame.fin:
raise InvalidFrameException(
'Received an unfragmented frame without '
'terminating existing fragmentation')
@ -347,31 +426,38 @@ class Stream(StreamBase):
'New fragmentation started without terminating '
'existing fragmentation')
if fin:
if frame.fin:
# Unfragmented frame
self._original_opcode = opcode
message = bytes
if is_control_opcode(opcode) and len(message) > 125:
if (common.is_control_opcode(frame.opcode) and
len(frame.payload) > 125):
raise InvalidFrameException(
'Application data size of control frames must be '
'125 bytes or less')
self._original_opcode = frame.opcode
message = frame.payload
else:
# Start of fragmentation frame
if is_control_opcode(opcode):
if common.is_control_opcode(frame.opcode):
raise InvalidFrameException(
'Control frames must not be fragmented')
self._original_opcode = opcode
self._received_fragments.append(bytes)
self._original_opcode = frame.opcode
self._received_fragments.append(frame.payload)
continue
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.
return message.decode('utf-8', 'replace')
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._request.client_terminated = True
@ -390,9 +476,13 @@ class Stream(StreamBase):
'!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)
self._logger.debug('Initiated flush read')
self.flushread()
# Drain junk data after the close frame if necessary.
self._drain_received_data()
if self._request.server_terminated:
self._logger.debug(
@ -403,7 +493,13 @@ class Stream(StreamBase):
self._logger.debug(
'Received client-initiated closing handshake')
self._send_closing_handshake(common.STATUS_NORMAL, '')
code = common.STATUS_NORMAL
reason = ''
if hasattr(self._request, '_dispatcher'):
dispatcher = self._request._dispatcher
code, reason = dispatcher.passive_closing_handshake(
self._request)
self._send_closing_handshake(code, reason)
self._logger.debug(
'Sent ack for client-initiated closing handshake')
return None
@ -464,7 +560,9 @@ class Stream(StreamBase):
'less')
frame = create_close_frame(
struct.pack('!H', code) + encoded_reason, self._options.mask_send)
struct.pack('!H', code) + encoded_reason,
self._options.mask_send,
self._options.outgoing_frame_filters)
self._request.server_terminated = True
@ -506,7 +604,10 @@ class Stream(StreamBase):
raise ValueError(
'Application data size of control frames must be 125 bytes or '
'less')
frame = create_ping_frame(body, self._options.mask_send)
frame = create_ping_frame(
body,
self._options.mask_send,
self._options.outgoing_frame_filters)
self._write(frame)
self._ping_queue.append(body)
@ -516,8 +617,31 @@ class Stream(StreamBase):
raise ValueError(
'Application data size of control frames must be 125 bytes or '
'less')
frame = create_pong_frame(body, self._options.mask_send)
frame = create_pong_frame(
body,
self._options.mask_send,
self._options.outgoing_frame_filters)
self._write(frame)
def _drain_received_data(self):
"""Drains unread data in the receive buffer to avoid sending out TCP
RST packet. This is because when deflate-stream is enabled, some
DEFLATE block for flushing data may follow a close frame. If any data
remains in the receive buffer of a socket when the socket is closed,
it sends out TCP RST packet to the other peer.
Since mod_python's mp_conn object doesn't support non-blocking read,
we perform this only when pywebsocket is running in standalone mode.
"""
# If self._options.deflate_stream is true, self._request is
# DeflateRequest, so we can get wrapped request object by
# self._request._request.
#
# Only _StandaloneRequest has _drain_received_data method.
if (self._options.deflate_stream and
('_drain_received_data' in dir(self._request._request))):
self._request._request._drain_received_data()
# vi:sts=4 sw=4 et

View File

@ -29,9 +29,21 @@
# Constants indicating WebSocket protocol version.
VERSION_HYBI07 = 8
VERSION_HYBI00 = 0
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
# Constants indicating WebSocket protocol latest version.
VERSION_HYBI_LATEST = VERSION_HYBI10
# Port numbers
DEFAULT_WEB_SOCKET_PORT = 80
@ -49,7 +61,7 @@ OPCODE_CLOSE = 0x8
OPCODE_PING = 0x9
OPCODE_PONG = 0xa
# UUIDs used by HyBi 07 opening handshake and frame masking.
# 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.
@ -59,19 +71,75 @@ 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_STREAM_EXTENSION = 'deflate-stream'
DEFLATE_FRAME_EXTENSION = 'deflate-frame'
# Status codes
# Code STATUS_CODE_NOT_AVAILABLE should not be used in actual frames. This code
# is exposed to JavaScript API as pseudo status code which represent actual
# frame does not have status code.
STATUS_NORMAL = 1000
STATUS_GOING_AWAY = 1001
STATUS_PROTOCOL_ERROR = 1002
STATUS_UNSUPPORTED = 1003
STATUS_TOO_LARGE = 1004
STATUS_CODE_NOT_AVAILABLE = 1005
STATUS_ABNORMAL_CLOSE = 1006
STATUS_INVALID_UTF8 = 1007
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
# vi:sts=4 sw=4 et

View File

@ -37,7 +37,9 @@ import os
import re
from mod_pywebsocket import common
from mod_pywebsocket import handshake
from mod_pywebsocket import msgutil
from mod_pywebsocket import stream
from mod_pywebsocket import util
@ -45,12 +47,21 @@ _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 DispatchError(Exception):
class DispatchException(Exception):
"""Exception in dispatching WebSocket request."""
pass
def __init__(self, name, status=404):
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, ''
def _normalize_path(path):
@ -65,8 +76,10 @@ def _normalize_path(path):
"""
path = path.replace('\\', os.path.sep)
# do not normalize away symlinks in mochitest
# path = os.path.realpath(path)
# MOZILLA: do not normalize away symlinks in mochitest
#path = os.path.realpath(path)
path = path.replace('\\', '/')
return path
@ -103,9 +116,11 @@ def _enumerate_handler_file_paths(directory):
class _HandlerSuite(object):
"""A handler suite holder class."""
def __init__(self, do_extra_handshake, transfer_data):
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):
@ -120,11 +135,19 @@ def _source_handler_file(handler_definition):
try:
exec handler_definition in global_dic
except Exception:
raise DispatchError('Error in sourcing handler:' +
util.get_stack_trace())
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))
_extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME),
passive_closing_handshake_handler)
def _extract_handler(dic, name):
@ -133,10 +156,10 @@ def _extract_handler(dic, name):
"""
if name not in dic:
raise DispatchError('%s is not defined.' % name)
raise DispatchException('%s is not defined.' % name)
handler = dic[name]
if not callable(handler):
raise DispatchError('%s is not callable.' % name)
raise DispatchException('%s is not callable.' % name)
return handler
@ -154,9 +177,10 @@ class Dispatcher(object):
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.
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.
"""
self._logger = util.get_class_logger(self)
@ -167,8 +191,8 @@ class Dispatcher(object):
scan_dir = root_dir
if not os.path.realpath(scan_dir).startswith(
os.path.realpath(root_dir)):
raise DispatchError('scan_dir:%s must be a directory under '
'root_dir:%s.' % (scan_dir, 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)
def add_resource_path_alias(self,
@ -186,7 +210,8 @@ class Dispatcher(object):
handler_suite = self._handler_suite_map[existing_resource_path]
self._handler_suite_map[alias_resource_path] = handler_suite
except KeyError:
raise DispatchError('No handler for: %r' % existing_resource_path)
raise DispatchException('No handler for: %r' %
existing_resource_path)
def source_warnings(self):
"""Return warnings in sourcing handlers."""
@ -201,19 +226,28 @@ class Dispatcher(object):
Args:
request: mod_python request.
Raises:
DispatchException: when handler was not found
AbortedByUserException: when user handler abort connection
HandshakeException: when opening handshake failed
"""
do_extra_handshake_ = self._get_handler_suite(
request).do_extra_handshake
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:
raise
except Exception, e:
util.prepend_message_to_exception(
'%s raised exception for %s: ' % (
_DO_EXTRA_HANDSHAKE_HANDLER_NAME,
request.ws_resource),
e)
raise
raise handshake.HandshakeException(e, 403)
def transfer_data(self, request):
"""Let a handler transfer_data with a WebSocket client.
@ -223,18 +257,28 @@ class Dispatcher(object):
Args:
request: mod_python request.
Raises:
DispatchException: when handler was not found
AbortedByUserException: when user handler abort connection
"""
transfer_data_ = self._get_handler_suite(request).transfer_data
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
# TODO(tyoshino): Terminate underlying TCP connection if possible.
try:
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', e)
raise
except msgutil.BadOperationException, e:
self._logger.debug('%s', e)
request.ws_stream.close_connection(common.STATUS_GOING_AWAY)
request.ws_stream.close_connection(common.STATUS_ABNORMAL_CLOSE)
except msgutil.InvalidFrameException, e:
# InvalidFrameException must be caught before
# ConnectionTerminatedException that catches InvalidFrameException.
@ -243,6 +287,9 @@ class Dispatcher(object):
except msgutil.UnsupportedFrameException, e:
self._logger.debug('%s', e)
request.ws_stream.close_connection(common.STATUS_UNSUPPORTED)
except stream.InvalidUTF8Exception, e:
self._logger_debug('%s', e)
request.ws_stream.close_connection(common.STATUS_INVALID_UTF8)
except msgutil.ConnectionTerminatedException, e:
self._logger.debug('%s', e)
except Exception, e:
@ -252,16 +299,31 @@ class Dispatcher(object):
e)
raise
def _get_handler_suite(self, request):
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.
"""
try:
ws_resource_path = request.ws_resource.split('?', 1)[0]
return self._handler_suite_map[ws_resource_path]
except KeyError:
raise DispatchError('No handler for: %r' % request.ws_resource)
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', 400);
return handler_suite
def _source_handler_files_in_dir(self, root_dir, scan_dir):
"""Source all the handler source files in the scan_dir directory.
@ -273,7 +335,7 @@ class Dispatcher(object):
for path in _enumerate_handler_file_paths(scan_dir):
try:
handler_suite = _source_handler_file(open(path).read())
except DispatchError, e:
except DispatchException, e:
self._source_warnings.append('%s: %s' % (path, e))
continue
self._handler_suite_map[convert(path)] = handler_suite

View File

@ -0,0 +1,201 @@
# 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 common
from mod_pywebsocket import util
_available_processors = {}
class ExtensionProcessorInterface(object):
def get_extension_response(self):
return None
def setup_stream_options(self, stream_options):
pass
class DeflateStreamExtensionProcessor(ExtensionProcessorInterface):
"""WebSocket DEFLATE stream extension processor."""
def __init__(self, request):
self._logger = util.get_class_logger(self)
self._request = request
def get_extension_response(self):
if len(self._request.get_parameter_names()) != 0:
return None
self._logger.debug(
'Enable %s extension', common.DEFLATE_STREAM_EXTENSION)
return common.ExtensionParameter(common.DEFLATE_STREAM_EXTENSION)
def setup_stream_options(self, stream_options):
stream_options.deflate_stream = True
_available_processors[common.DEFLATE_STREAM_EXTENSION] = (
DeflateStreamExtensionProcessor)
class DeflateFrameExtensionProcessor(ExtensionProcessorInterface):
"""WebSocket Per-frame DEFLATE extension processor."""
_WINDOW_BITS_PARAM = 'window_bits'
_NO_CONTEXT_TAKEOVER_PARAM = 'no_context_takeover'
def __init__(self, request):
self._logger = util.get_class_logger(self)
self._request = request
self._response_window_bits = None
self._response_no_context_takeover = False
def get_extension_response(self):
# Any unknown parameter will be just ignored.
window_bits = self._request.get_parameter_value(
self._WINDOW_BITS_PARAM)
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
if window_bits is not None:
try:
window_bits = int(window_bits)
except ValueError, e:
return None
if window_bits < 8 or window_bits > 15:
return None
self._deflater = util._RFC1979Deflater(
window_bits, no_context_takeover)
self._inflater = util._RFC1979Inflater()
self._compress_outgoing = True
response = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
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)' %
(common.DEFLATE_STREAM_EXTENSION,
window_bits,
no_context_takeover,
self._response_window_bits,
self._response_no_context_takeover))
return response
def setup_stream_options(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 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.
"""
if (not self._compress_outgoing or
common.is_control_opcode(frame.opcode)):
return
frame.payload = self._deflater.filter(frame.payload)
frame.rsv1 = 1
def _incoming_filter(self, frame):
"""Transform incoming frames. This method is called only by
an _IncomingFilter instance.
"""
if frame.rsv1 != 1 or common.is_control_opcode(frame.opcode):
return
frame.payload = self._inflater.filter(frame.payload)
frame.rsv1 = 0
_available_processors[common.DEFLATE_FRAME_EXTENSION] = (
DeflateFrameExtensionProcessor)
def get_extension_processor(extension_request):
global _available_processors
processor_class = _available_processors.get(extension_request.name())
if processor_class is None:
return None
return processor_class(extension_request)
# vi:sts=4 sw=4 et

View File

@ -36,81 +36,73 @@ successfully established.
import logging
from mod_pywebsocket import util
from mod_pywebsocket.handshake import draft75
from mod_pywebsocket.handshake import hybi00
from mod_pywebsocket.handshake import hybi06
# Export Extension symbol from this module.
from mod_pywebsocket.handshake._base import Extension
# Export HandshakeError symbol from this module.
from mod_pywebsocket.handshake._base import HandshakeError
from mod_pywebsocket.handshake import hybi
# Export AbortedByUserException and HandshakeException symbol from this module.
from mod_pywebsocket.handshake._base import AbortedByUserException
from mod_pywebsocket.handshake._base import HandshakeException
class Handshaker(object):
"""This class performs WebSocket handshake."""
_LOGGER = logging.getLogger(__name__)
def __init__(self, request, dispatcher, allowDraft75=False, strict=False):
"""Construct an instance.
Args:
request: mod_python request.
dispatcher: Dispatcher (dispatch.Dispatcher).
allowDraft75: allow draft 75 handshake protocol.
strict: Strictly check handshake request in draft 75.
Default: False. If True, request.connection must provide
get_memorized_lines method.
def do_handshake(request, dispatcher, allowDraft75=False, strict=False):
"""Performs WebSocket handshake.
Handshaker will add attributes such as ws_resource in performing
handshake.
"""
Args:
request: mod_python request.
dispatcher: Dispatcher (dispatch.Dispatcher).
allowDraft75: allow draft 75 handshake protocol.
strict: Strictly check handshake request in draft 75.
Default: False. If True, request.connection must provide
get_memorized_lines method.
self._logger = util.get_class_logger(self)
Handshaker will add attributes such as ws_resource in performing
handshake.
"""
self._request = request
self._dispatcher = dispatcher
self._strict = strict
self._hybi07Handshaker = hybi06.Handshaker(request, dispatcher)
self._hybi00Handshaker = hybi00.Handshaker(request, dispatcher)
self._hixie75Handshaker = None
if allowDraft75:
self._hixie75Handshaker = draft75.Handshaker(
request, dispatcher, strict)
_LOGGER.debug('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(
'Opening handshake request headers: %r', dict(request.headers_in))
def do_handshake(self):
"""Perform WebSocket Handshake."""
handshakers = []
handshakers.append(
('IETF HyBi latest', hybi.Handshaker(request, dispatcher)))
handshakers.append(
('IETF HyBi 00', hybi00.Handshaker(request, dispatcher)))
if allowDraft75:
handshakers.append(
('IETF Hixie 75', draft75.Handshaker(request, dispatcher, strict)))
self._logger.debug('Opening handshake resource: %r', self._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.
self._logger.debug(
'Opening handshake request headers: %r',
dict(self._request.headers_in))
for name, handshaker in handshakers:
_LOGGER.info('Trying %s protocol', name)
try:
handshaker.do_handshake()
return
except HandshakeException, e:
_LOGGER.info(
'Failed to complete opening handshake as %s protocol: %r',
name, e)
if e.status:
raise e
except AbortedByUserException, e:
raise
handshakers = [
('HyBi 07', self._hybi07Handshaker),
('HyBi 00', self._hybi00Handshaker),
('Hixie 75', self._hixie75Handshaker)]
last_error = HandshakeError('No handshaker available')
for name, handshaker in handshakers:
if handshaker:
self._logger.info('Trying %s protocol', name)
try:
handshaker.do_handshake()
return
except HandshakeError, e:
self._logger.info('%s handshake failed: %s', name, e)
last_error = e
raise last_error
raise HandshakeException(
'Failed to complete opening handshake for all available protocols')
# vi:sts=4 sw=4 et

View File

@ -37,50 +37,28 @@ from mod_pywebsocket import common
from mod_pywebsocket import http_header_util
class Extension(object):
"""Holds information about an extension which is exchanged on extension
negotiation in opening handshake.
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.
"""
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_parameter(self, name):
for param_name, param_value in self._parameters:
if param_name == name:
return param_value
def get_parameter_names(self):
return [name for name, unused_value in self._parameters]
def get_formatted_string(self):
formatted_params = [self._name]
for param_name, param_value in self._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)
pass
class HandshakeError(Exception):
class HandshakeException(Exception):
"""This exception will be raised when an error occurred while processing
WebSocket initial handshake.
"""
pass
def __init__(self, name, status=None):
super(HandshakeException, self).__init__(name)
self.status = status
def get_default_port(is_secure):
@ -90,23 +68,35 @@ def get_default_port(is_secure):
return common.DEFAULT_WEB_SOCKET_PORT
# TODO(tyoshino): Have stricter validator for HyBi 07.
def validate_subprotocol(subprotocol):
def validate_subprotocol(subprotocol, hixie):
"""Validate a value in subprotocol fields such as WebSocket-Protocol,
Sec-WebSocket-Protocol.
See
- HyBi 06: Section 5.2.2.
- HyBi 10: Section 5.1. and 5.2.2.
- HyBi 00: Section 4.1. Opening handshake
- Hixie 75: Section 4.1. Handshake
"""
if not subprotocol:
raise HandshakeError('Invalid subprotocol name: empty')
for c in subprotocol:
if not 0x20 <= ord(c) <= 0x7e:
raise HandshakeError(
'Illegal character in subprotocol name: %r' % c)
raise HandshakeException('Invalid subprotocol name: empty')
if hixie:
# 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)
else:
# 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):
@ -116,7 +106,7 @@ def parse_host_header(request):
try:
return fields[0], int(fields[1])
except ValueError, e:
raise HandshakeError('Invalid port number format: %r' % e)
raise HandshakeException('Invalid port number format: %r' % e)
def format_header(name, value):
@ -134,8 +124,8 @@ def build_location(request):
host, port = parse_host_header(request)
connection_port = request.connection.local_addr[1]
if port != connection_port:
raise HandshakeError('Header/connection port mismatch: %d/%d' %
(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(':')
@ -147,24 +137,24 @@ def build_location(request):
def get_mandatory_header(request, key):
value = request.headers_in.get(key)
if value is None:
raise HandshakeError('Header %s is not defined' % key)
raise HandshakeException('Header %s is not defined' % key)
return value
def validate_mandatory_header(request, key, expected_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 HandshakeError(
raise HandshakeException(
'Expected %r for header %s but found %r (case-insensitive)' %
(expected_value, key, value))
(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 HandshakeError('Method is not GET')
raise HandshakeException('Method is not GET')
def check_header_lines(request, mandatory_headers):
@ -199,13 +189,13 @@ def parse_token_list(data):
break
if not http_header_util.consume_string(state, ','):
raise HandshakeError(
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 HandshakeError('No valid token found')
raise HandshakeException('No valid token found')
return token_list
@ -214,7 +204,7 @@ def _parse_extension_param(state, definition):
param_name = http_header_util.consume_token(state)
if param_name is None:
raise HandshakeError('No valid parameter name found')
raise HandshakeException('No valid parameter name found')
http_header_util.consume_lwses(state)
@ -226,7 +216,7 @@ def _parse_extension_param(state, definition):
param_value = http_header_util.consume_token_or_quoted_string(state)
if param_value is None:
raise HandshakeError(
raise HandshakeException(
'No valid parameter value found on the right-hand side of '
'parameter %r' % param_name)
@ -238,7 +228,7 @@ def _parse_extension(state):
if extension_token is None:
return None
extension = Extension(extension_token)
extension = common.ExtensionParameter(extension_token)
while True:
http_header_util.consume_lwses(state)
@ -250,8 +240,8 @@ def _parse_extension(state):
try:
_parse_extension_param(state, extension)
except HandshakeError, e:
raise HandshakeError(
except HandshakeException, e:
raise HandshakeException(
'Failed to parse Sec-WebSocket-Extensions header: '
'Failed to parse parameter for %r (%r)' %
(extension_token, e))
@ -261,7 +251,7 @@ def _parse_extension(state):
def parse_extensions(data):
"""Parses Sec-WebSocket-Extensions header value returns a list of
common.Extension objects.
common.ExtensionParameter objects.
Leading LWSes must be trimmed.
"""
@ -280,7 +270,7 @@ def parse_extensions(data):
break
if not http_header_util.consume_string(state, ','):
raise HandshakeError(
raise HandshakeException(
'Failed to parse Sec-WebSocket-Extensions header: '
'Expected a comma but found %r' %
http_header_util.peek(state))
@ -288,7 +278,7 @@ def parse_extensions(data):
http_header_util.consume_lwses(state)
if len(extension_list) == 0:
raise HandshakeError(
raise HandshakeException(
'Sec-WebSocket-Extensions header contains no valid extension')
return extension_list
@ -297,7 +287,15 @@ def parse_extensions(data):
def format_extensions(extension_list):
formatted_extension_list = []
for extension in extension_list:
formatted_extension_list.append(extension.get_formatted_string())
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))
formatted_extension_list.append('; '.join(formatted_params))
return ', '.join(formatted_extension_list)

View File

@ -43,7 +43,7 @@ import re
from mod_pywebsocket import common
from mod_pywebsocket.stream import StreamHixie75
from mod_pywebsocket import util
from mod_pywebsocket.handshake._base import HandshakeError
from mod_pywebsocket.handshake._base import HandshakeException
from mod_pywebsocket.handshake._base import build_location
from mod_pywebsocket.handshake._base import validate_subprotocol
@ -131,7 +131,7 @@ class Handshaker(object):
def _set_subprotocol(self):
subprotocol = self._request.headers_in.get('WebSocket-Protocol')
if subprotocol is not None:
validate_subprotocol(subprotocol)
validate_subprotocol(subprotocol, hixie=True)
self._request.ws_protocol = subprotocol
def _set_protocol_version(self):
@ -157,10 +157,10 @@ class Handshaker(object):
for key, expected_value in _MANDATORY_HEADERS:
actual_value = self._request.headers_in.get(key)
if not actual_value:
raise HandshakeError('Header %s is not defined' % key)
raise HandshakeException('Header %s is not defined' % key)
if expected_value:
if actual_value != expected_value:
raise HandshakeError(
raise HandshakeException(
'Expected %r for header %s but found %r' %
(expected_value, key, actual_value))
if self._strict:
@ -174,16 +174,17 @@ class Handshaker(object):
def _check_first_lines(self, lines):
if len(lines) < len(_FIRST_FIVE_LINES):
raise HandshakeError('Too few header lines: %d' % len(lines))
raise HandshakeException('Too few header lines: %d' % len(lines))
for line, regexp in zip(lines, _FIRST_FIVE_LINES):
if not regexp.search(line):
raise HandshakeError('Unexpected header: %r doesn\'t match %r'
% (line, regexp.pattern))
raise HandshakeException(
'Unexpected header: %r doesn\'t match %r'
% (line, regexp.pattern))
sixth_and_later = ''.join(lines[5:])
if not _SIXTH_AND_LATER.search(sixth_and_later):
raise HandshakeError('Unexpected header: %r doesn\'t match %r'
% (sixth_and_later,
_SIXTH_AND_LATER.pattern))
raise HandshakeException(
'Unexpected header: %r doesn\'t match %r'
% (sixth_and_later, _SIXTH_AND_LATER.pattern))
# vi:sts=4 sw=4 et

View File

@ -28,7 +28,7 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""WebSocket HyBi 07 opening handshake processor."""
"""WebSocket HyBi latest opening handshake processor."""
# Note: request.connection.write is used in this module, even though mod_python
@ -43,18 +43,19 @@ import os
import re
from mod_pywebsocket import common
from mod_pywebsocket.stream import Stream
from mod_pywebsocket.stream import StreamOptions
from mod_pywebsocket import util
from mod_pywebsocket.extensions import get_extension_processor
from mod_pywebsocket.handshake._base import check_request_line
from mod_pywebsocket.handshake._base import Extension
from mod_pywebsocket.handshake._base import format_extensions
from mod_pywebsocket.handshake._base import format_header
from mod_pywebsocket.handshake._base import get_mandatory_header
from mod_pywebsocket.handshake._base import HandshakeError
from mod_pywebsocket.handshake._base import HandshakeException
from mod_pywebsocket.handshake._base import parse_extensions
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.stream import Stream
from mod_pywebsocket.stream import StreamOptions
from mod_pywebsocket import util
_BASE64_REGEX = re.compile('^[+/0-9A-Za-z]*=*$')
@ -90,21 +91,14 @@ class Handshaker(object):
self._request = request
self._dispatcher = dispatcher
def do_handshake(self):
check_request_line(self._request)
validate_mandatory_header(
self._request,
common.UPGRADE_HEADER,
common.WEBSOCKET_UPGRADE_TYPE)
def _validate_connection_header(self):
connection = get_mandatory_header(
self._request, common.CONNECTION_HEADER)
try:
connection_tokens = parse_token_list(connection)
except HandshakeError, e:
raise HandshakeError(
except HandshakeException, e:
raise HandshakeException(
'Failed to parse %s: %s' % (common.CONNECTION_HEADER, e))
connection_is_valid = False
@ -113,55 +107,118 @@ class Handshaker(object):
connection_is_valid = True
break
if not connection_is_valid:
raise HandshakeError(
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._get_origin()
self._check_version()
self._set_protocol()
self._set_extensions()
key = self._get_key()
(accept, accept_binary) = compute_accept(key)
self._logger.debug('Sec-WebSocket-Accept: %r (%s)',
accept, util.hexify(accept_binary))
# 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()
self._parse_extensions()
self._logger.debug('IETF HyBi 07 protocol')
self._request.ws_version = common.VERSION_HYBI07
stream_options = StreamOptions()
stream_options.deflate = self._request.ws_deflate
self._request.ws_stream = Stream(self._request, stream_options)
self._request.ws_close_code = None
self._request.ws_close_reason = None
self._dispatcher.do_extra_handshake(self._request)
if self._request.ws_requested_protocols is not None:
if self._request.ws_protocol is None:
raise HandshakeError(
'do_extra_handshake must choose one subprotocol from '
'ws_requested_protocols and set it to ws_protocol')
# TODO(tyoshino): Validate selected subprotocol value.
# Key validation, response generation.
key = self._get_key()
(accept, accept_binary) = compute_accept(key)
self._logger.debug(
'Subprotocol accepted: %r',
self._request.ws_protocol)
else:
if self._request.ws_protocol is not None:
raise HandshakeError(
'ws_protocol must be None when the client didn\'t request '
'any subprotocol')
'%s: %r (%s)',
common.SEC_WEBSOCKET_ACCEPT_HEADER,
accept,
util.hexify(accept_binary))
self._send_handshake(accept)
self._logger.debug('IETF HyBi protocol')
self._request.ws_version = common.VERSION_HYBI_LATEST
self._logger.debug('Sent opening handshake response')
# 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
# Extra handshake handler may modify/remove processors.
self._dispatcher.do_extra_handshake(self._request)
stream_options = StreamOptions()
self._request.ws_extensions = None
for processor in self._request.ws_extension_processors:
if processor is None:
# Some processors may be removed by extra handshake
# handler.
continue
extension_response = processor.get_extension_response()
if extension_response is None:
# Rejected.
continue
if self._request.ws_extensions is None:
self._request.ws_extensions = []
self._request.ws_extensions.append(extension_response)
processor.setup_stream_options(stream_options)
if self._request.ws_extensions is not None:
self._logger.debug(
'Extensions accepted: %r',
map(common.ExtensionParameter.name,
self._request.ws_extensions))
self._request.ws_stream = Stream(self._request, stream_options)
if self._request.ws_requested_protocols is not None:
if self._request.ws_protocol is None:
raise HandshakeException(
'do_extra_handshake must choose one subprotocol from '
'ws_requested_protocols and set it to ws_protocol')
validate_subprotocol(self._request.ws_protocol, hixie=False)
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)
self._logger.debug('Sent opening handshake response')
except HandshakeException, e:
if not e.status:
# Fallback to 400 bad request by default.
e.status = 400
raise e
def _get_origin(self):
origin = self._request.headers_in.get(
@ -170,7 +227,8 @@ class Handshaker(object):
def _check_version(self):
unused_value = validate_mandatory_header(
self._request, common.SEC_WEBSOCKET_VERSION_HEADER, '8')
self._request, common.SEC_WEBSOCKET_VERSION_HEADER,
str(common.VERSION_HYBI_LATEST), fail_status=426)
def _set_protocol(self):
self._request.ws_protocol = None
@ -182,45 +240,25 @@ class Handshaker(object):
self._request.ws_requested_protocols = None
return
# TODO(tyoshino): Validate the header value.
requested_protocols = protocol_header.split(',')
self._request.ws_requested_protocols = [
s.strip() for s in requested_protocols]
self._logger.debug('Subprotocols requested: %r', requested_protocols)
def _set_extensions(self):
self._request.ws_deflate = False
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
self._request.ws_extensions = None
return
self._request.ws_extensions = []
requested_extensions = parse_extensions(extensions_header)
for extension in requested_extensions:
extension_name = extension.name()
# We now support only deflate-stream extension. Any other
# extension requests are just ignored for now.
if (extension_name == 'deflate-stream' and
len(extension.get_parameter_names()) == 0):
self._request.ws_extensions.append(extension)
self._request.ws_deflate = True
self._request.ws_requested_extensions = requested_extensions
self._request.ws_requested_extensions = parse_extensions(
extensions_header)
self._logger.debug(
'Extensions requested: %r',
map(Extension.name, self._request.ws_requested_extensions))
self._logger.debug(
'Extensions accepted: %r',
map(Extension.name, self._request.ws_extensions))
map(common.ExtensionParameter.name,
self._request.ws_requested_extensions))
def _validate_key(self, key):
# Validate
@ -238,7 +276,7 @@ class Handshaker(object):
pass
if not key_is_valid:
raise HandshakeError(
raise HandshakeException(
'Illegal value for header %s: %r' %
(common.SEC_WEBSOCKET_KEY_HEADER, key))
@ -250,8 +288,11 @@ class Handshaker(object):
decoded_key = self._validate_key(key)
self._logger.debug('Sec-WebSocket-Key: %r (%s)',
key, util.hexify(decoded_key))
self._logger.debug(
'%s: %r (%s)',
common.SEC_WEBSOCKET_KEY_HEADER,
key,
util.hexify(decoded_key))
return key
@ -266,13 +307,12 @@ class Handshaker(object):
common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE))
response.append(format_header(
common.SEC_WEBSOCKET_ACCEPT_HEADER, accept))
# TODO(tyoshino): Encode value of protocol and extensions if any
# special character that we have to encode by some manner.
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:
if (self._request.ws_extensions is not None and
len(self._request.ws_extensions) != 0):
response.append(format_header(
common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
format_extensions(self._request.ws_extensions)))

View File

@ -45,7 +45,7 @@ import struct
from mod_pywebsocket import common
from mod_pywebsocket.stream import StreamHixie75
from mod_pywebsocket import util
from mod_pywebsocket.handshake._base import HandshakeError
from mod_pywebsocket.handshake._base import HandshakeException
from mod_pywebsocket.handshake._base import build_location
from mod_pywebsocket.handshake._base import check_header_lines
from mod_pywebsocket.handshake._base import format_header
@ -87,6 +87,10 @@ class Handshaker(object):
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.
@ -113,7 +117,7 @@ class Handshaker(object):
subprotocol = self._request.headers_in.get(
common.SEC_WEBSOCKET_PROTOCOL_HEADER)
if subprotocol is not None:
validate_subprotocol(subprotocol)
validate_subprotocol(subprotocol, hixie=True)
self._request.ws_protocol = subprotocol
def _set_location(self):
@ -125,29 +129,16 @@ class Handshaker(object):
def _set_origin(self):
# |Origin|
origin = self._request.headers_in['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('Sec-WebSocket-Draft')
if draft is not None:
try:
draft_int = int(draft)
# Draft value 2 is used by HyBi 02 and 03 which we no longer
# support. draft >= 3 and <= 1 are never defined in the spec.
# 0 might be used to mean HyBi 00 by somebody. 1 might be used
# to mean HyBi 01 by somebody but we no longer support it.
if draft_int == 1 or draft_int == 2:
raise HandshakeError('HyBi 01-03 are not supported')
elif draft_int != 0:
raise ValueError
except ValueError, e:
raise HandshakeError(
'Illegal value for Sec-WebSocket-Draft: %s' % 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('IETF HyBi 00 protocol')
self._request.ws_version = common.VERSION_HYBI00
@ -180,12 +171,12 @@ class Handshaker(object):
try:
key_number = int(re.sub("\\D", "", key_value))
except:
raise HandshakeError('%s field contains no digit' % key_field)
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 HandshakeError('%s field contains no space' % key_field)
raise HandshakeException('%s field contains no space' % key_field)
self._logger.debug(
'%s: Key-number is %d and number of spaces is %d',
@ -194,7 +185,7 @@ class Handshaker(object):
# 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 HandshakeError(
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/.
@ -204,8 +195,8 @@ class Handshaker(object):
def _get_challenge(self):
# 5.2 4-7.
key1 = self._get_key_value('Sec-WebSocket-Key1')
key2 = self._get_key_value('Sec-WebSocket-Key2')
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
@ -225,7 +216,7 @@ class Handshaker(object):
response.append(format_header(
common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE))
response.append(format_header(
'Sec-WebSocket-Location', self._request.ws_location))
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:

View File

@ -134,34 +134,39 @@ def headerparserhandler(request):
Args:
request: mod_python request.
This function is named headerparserhandler because it is the default name
for a PythonHeaderParserHandler.
This function is named headerparserhandler because it is the default
name for a PythonHeaderParserHandler.
"""
handshake_is_done = False
try:
allowDraft75 = apache.main_server.get_options().get(
_PYOPT_ALLOW_DRAFT75, None)
handshaker = handshake.Handshaker(request, _dispatcher,
allowDraft75=allowDraft75)
handshaker.do_handshake()
handshake.do_handshake(
request, _dispatcher, allowDraft75=allowDraft75)
handshake_is_done = True
request.log_error(
'mod_pywebsocket: resource: %r' % request.ws_resource,
apache.APLOG_DEBUG)
try:
_dispatcher.transfer_data(request)
except Exception, e:
# Catch exception in transfer_data.
# In this case, handshake has been successful, so just log the
# exception and return apache.DONE
request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_WARNING)
except handshake.HandshakeError, e:
# Handshake for ws/wss failed.
# But the request can be valid http/https request.
request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_INFO)
return apache.DECLINED
except dispatch.DispatchError, e:
request._dispatcher = _dispatcher
_dispatcher.transfer_data(request)
except dispatch.DispatchException, e:
request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_WARNING)
return apache.DECLINED
if not handshake_is_done:
return e.status
except handshake.AbortedByUserException, e:
request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_INFO)
except handshake.HandshakeException, e:
# Handshake for ws/wss failed.
# The request handling fallback into http/https.
request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_INFO)
return e.status
except Exception, e:
request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_WARNING)
# Unknown exceptions before handshake mean Apache must handle its
# request with another handler.
if not handshake_is_done:
return apache.DECLINE
# Set assbackwards to suppress response header generation by Apache.
request.assbackwards = 1
return apache.DONE # Return DONE such that no other handlers are invoked.

View File

@ -33,6 +33,9 @@ in HTTP RFC http://www.ietf.org/rfc/rfc2616.txt.
"""
import urlparse
_SEPARATORS = '()<>@,;:\\"/[]?={} \t'
@ -78,7 +81,8 @@ def consume(state, amount=1):
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.
the head. Returns True if consumed successfully. Otherwise, returns
False.
"""
pos = 0
@ -210,4 +214,41 @@ def quote_if_necessary(s):
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
# vi:sts=4 sw=4 et

View File

@ -60,19 +60,35 @@ class MemorizingFile(object):
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',
'readline', 'get_memorized_lines'):
'_buffered', '_buffered_line', 'readline',
'get_memorized_lines'):
return object.__getattribute__(self, name)
return self._file.__getattribute__(name)
def readline(self):
"""Override file.readline and memorize the line read."""
def readline(self, size=-1):
"""Override file.readline and memorize the line read.
line = self._file.readline()
if line and len(self._memorized_lines) < self._max_memorized_lines:
self._memorized_lines.append(line)
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):

View File

@ -31,9 +31,10 @@
"""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.
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.
"""
@ -58,23 +59,25 @@ def close_connection(request):
request.ws_stream.close_connection()
def send_message(request, message, end=True):
def send_message(request, message, end=True, binary=False):
"""Send message.
Args:
request: mod_python request.
message: unicode string to send.
end: False to send message as a fragment. All messages until the first
call with end=True (inclusive) will be delivered to the client
in separate frames but as one WebSocket message.
message: unicode text or str binary to send.
end: False to send message as a fragment. All messages until the
first call with end=True (inclusive) will be delivered to the
client in separate frames but as one WebSocket message.
binary: send message as binary frame.
Raises:
BadOperationException: when server already terminated.
"""
request.ws_stream.send_message(message, end)
request.ws_stream.send_message(message, end, binary)
def receive_message(request):
"""Receive a WebSocket frame and return its payload as unicode string.
"""Receive a WebSocket frame and return its payload as a text in
unicode or a binary in str.
Args:
request: mod_python request.
@ -91,8 +94,8 @@ def send_ping(request, 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.
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
@ -107,8 +110,8 @@ class MessageReceiver(threading.Thread):
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.
and MessageReceiver.receive_nowait are useless
because they will never return any messages.
"""
threading.Thread.__init__(self)

View File

@ -1,4 +1,4 @@
# Copyright 2010, Google Inc.
# Copyright 2011, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
@ -35,19 +35,22 @@
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_hybi06 import Stream
from mod_pywebsocket._stream_hybi06 import StreamOptions
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_hybi06 import create_close_frame
from mod_pywebsocket._stream_hybi06 import create_header
from mod_pywebsocket._stream_hybi06 import create_length_header
from mod_pywebsocket._stream_hybi06 import create_ping_frame
from mod_pywebsocket._stream_hybi06 import create_pong_frame
from mod_pywebsocket._stream_hybi06 import create_text_frame
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
# vi:sts=4 sw=4 et

View File

@ -33,6 +33,7 @@
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
@ -51,6 +52,7 @@ import StringIO
import logging
import os
import re
import socket
import traceback
import zlib
@ -163,8 +165,8 @@ class NoopMasker(object):
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.
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, mask):
@ -216,11 +218,11 @@ class DeflateRequest(object):
class _Deflater(object):
def __init__(self):
def __init__(self, window_bits):
self._logger = get_class_logger(self)
self._compress = zlib.compressobj(
zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -window_bits)
def compress_and_flush(self, bytes):
compressed_bytes = self._compress.compress(bytes)
@ -292,6 +294,45 @@ class _Inflater(object):
self._decompress = zlib.decompressobj(-zlib.MAX_WBITS)
# 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):
if self._deflater is None or self._no_context_takeover:
self._deflater = _Deflater(self._window_bits)
# 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]
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):
self._inflater = _Inflater()
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.
@ -305,13 +346,13 @@ class DeflateSocket(object):
self._logger = get_class_logger(self)
self._deflater = _Deflater()
self._deflater = _Deflater(zlib.MAX_WBITS)
self._inflater = _Inflater()
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.
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
@ -346,7 +387,7 @@ class DeflateConnection(object):
self._logger = get_class_logger(self)
self._deflater = _Deflater()
self._deflater = _Deflater(zlib.MAX_WBITS)
self._inflater = _Inflater()
def put_bytes(self, bytes):
@ -388,15 +429,55 @@ class DeflateConnection(object):
def write(self, bytes):
self._connection.write(self._deflater.compress_and_flush(bytes))
def flushread(self):
self._connection.setblocking(0)
while True:
try:
data = self._connection.read(1)
self._logger.debug('flushing unused byte %r', data)
if len(data) < 1:
def _is_ewouldblock_errno(error_number):
"""Returns True iff error_number indicates that receive operation would
block. To make this portable, we check availability of errno and then
compare them.
"""
for error_name in ['WSAEWOULDBLOCK', 'EWOULDBLOCK', 'EAGAIN']:
if (error_name in dir(errno) and
error_number == getattr(errno, error_name)):
return True
return False
def drain_received_data(raw_socket):
# Set the socket non-blocking.
original_timeout = raw_socket.gettimeout()
raw_socket.settimeout(0.0)
drained_data = []
# Drain until the socket is closed or no data is immediately
# available for read.
while True:
try:
data = raw_socket.recv(1)
if not data:
break
drained_data.append(data)
except socket.error, e:
# e can be either a pair (errno, string) or just a string (or
# something else) telling what went wrong. We suppress only
# the errors that indicates that the socket blocks. Those
# exceptions can be parsed as a pair (errno, string).
try:
error_number, message = e
except:
break
# Failed to parse socket.error.
raise e
if _is_ewouldblock_errno(error_number):
break
else:
raise e
# Rollback timeout value.
raw_socket.settimeout(original_timeout)
return ''.join(drained_data)
# vi:sts=4 sw=4 et

335
testing/mochitest/pywebsocket/standalone.py Normal file → Executable file
View File

@ -49,8 +49,8 @@ Usage:
See __init__.py for details of <websock_handlers> and how to write WebSocket
handlers. If this path is relative, <document_root> is used as the base.
<scan_dir> is a path under the root directory. If specified, only the handlers
under scan_dir are scanned. This is useful in saving scan time.
<scan_dir> is a path under the root directory. If specified, only the
handlers under scan_dir are scanned. This is useful in saving scan time.
Note:
This server is derived from SocketServer.ThreadingMixIn. Hence a thread is
@ -70,8 +70,10 @@ import logging.handlers
import optparse
import os
import re
import select
import socket
import sys
import threading
_HAS_OPEN_SSL = False
try:
@ -83,6 +85,7 @@ except ImportError:
from mod_pywebsocket import common
from mod_pywebsocket import dispatch
from mod_pywebsocket import handshake
from mod_pywebsocket import http_header_util
from mod_pywebsocket import memorizingfile
from mod_pywebsocket import util
@ -112,10 +115,12 @@ class _StandaloneConnection(object):
Args:
request_handler: A WebSocketRequestHandler instance.
"""
self._request_handler = request_handler
def get_local_addr(self):
"""Getter to mimic mp_conn.local_addr."""
return (self._request_handler.server.server_name,
self._request_handler.server.server_port)
local_addr = property(get_local_addr)
@ -125,23 +130,25 @@ class _StandaloneConnection(object):
Setting the property in __init__ won't work because the request
handler is not initialized yet there."""
return self._request_handler.client_address
remote_addr = property(get_remote_addr)
def write(self, data):
"""Mimic mp_conn.write()."""
return self._request_handler.wfile.write(data)
def read(self, length):
"""Mimic mp_conn.read()."""
return self._request_handler.rfile.read(length)
def get_memorized_lines(self):
"""Get memorized lines."""
return self._request_handler.rfile.get_memorized_lines()
def setblocking(self, blocking):
self._request_handler.rfile._file._sock.setblocking(0)
class _StandaloneRequest(object):
"""Mimic mod_python request."""
@ -152,56 +159,153 @@ class _StandaloneRequest(object):
Args:
request_handler: A WebSocketRequestHandler instance.
"""
self._logger = util.get_class_logger(self)
self._request_handler = request_handler
self.connection = _StandaloneConnection(request_handler)
self._use_tls = use_tls
def get_uri(self):
"""Getter to mimic request.uri."""
return self._request_handler.path
uri = property(get_uri)
def get_method(self):
"""Getter to mimic request.method."""
return self._request_handler.command
method = property(get_method)
def get_headers_in(self):
"""Getter to mimic request.headers_in."""
return self._request_handler.headers
headers_in = property(get_headers_in)
def is_https(self):
"""Mimic request.is_https()."""
return self._use_tls
def _drain_received_data(self):
"""Don't use this method from WebSocket handler. Drains unread data
in the receive buffer.
"""
raw_socket = self._request_handler.connection
drained_data = util.drain_received_data(raw_socket)
if drained_data:
self._logger.debug(
'Drained data following close frame: %r', drained_data)
class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
"""HTTPServer specialized for WebSocket."""
# Overrides SocketServer.ThreadingMixIn.daemon_threads
daemon_threads = True
# Overrides BaseHTTPServer.HTTPServer.allow_reuse_address
allow_reuse_address = True
def __init__(self, server_address, RequestHandlerClass):
"""Override SocketServer.TCPServer.__init__ to set SSL enabled socket
object to self.socket before server_bind and server_activate, if
necessary.
def __init__(self, options):
"""Override SocketServer.TCPServer.__init__ to set SSL enabled
socket object to self.socket before server_bind and server_activate,
if necessary.
"""
self.request_queue_size = options.request_queue_size
self.__ws_is_shut_down = threading.Event()
self.__ws_serving = False
SocketServer.BaseServer.__init__(
self, server_address, RequestHandlerClass)
self.socket = self._create_socket()
self, (options.server_host, options.port), WebSocketRequestHandler)
# Expose the options object to allow handler objects access it. We name
# it with websocket_ prefix to avoid conflict.
self.websocket_server_options = options
self._create_sockets()
self.server_bind()
self.server_activate()
def _create_socket(self):
socket_ = socket.socket(self.address_family, self.socket_type)
if WebSocketServer.options.use_tls:
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
ctx.use_privatekey_file(WebSocketServer.options.private_key)
ctx.use_certificate_file(WebSocketServer.options.certificate)
socket_ = OpenSSL.SSL.Connection(ctx, socket_)
return socket_
def _create_sockets(self):
self.server_name, self.server_port = self.server_address
self._sockets = []
if not self.server_name:
addrinfo_array = [
(self.address_family, self.socket_type, '', '', '')]
else:
addrinfo_array = socket.getaddrinfo(self.server_name,
self.server_port,
socket.AF_UNSPEC,
socket.SOCK_STREAM,
socket.IPPROTO_TCP)
for addrinfo in addrinfo_array:
logging.info('Create socket on: %r', addrinfo)
family, socktype, proto, canonname, sockaddr = addrinfo
try:
socket_ = socket.socket(family, socktype)
except Exception, e:
logging.info('Skip by failure: %r', e)
continue
if self.websocket_server_options.use_tls:
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
ctx.use_privatekey_file(
self.websocket_server_options.private_key)
ctx.use_certificate_file(
self.websocket_server_options.certificate)
socket_ = OpenSSL.SSL.Connection(ctx, socket_)
self._sockets.append((socket_, addrinfo))
def server_bind(self):
"""Override SocketServer.TCPServer.server_bind to enable multiple
sockets bind.
"""
for socket_, addrinfo in self._sockets:
logging.info('Bind on: %r', addrinfo)
if self.allow_reuse_address:
socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
socket_.bind(self.server_address)
def server_activate(self):
"""Override SocketServer.TCPServer.server_activate to enable multiple
sockets listen.
"""
failed_sockets = []
for socketinfo in self._sockets:
socket_, addrinfo = socketinfo
logging.info('Listen on: %r', addrinfo)
try:
socket_.listen(self.request_queue_size)
except Exception, e:
logging.info('Skip by failure: %r', e)
socket_.close()
failed_sockets.append(socketinfo)
for socketinfo in failed_sockets:
self._sockets.remove(socketinfo)
def server_close(self):
"""Override SocketServer.TCPServer.server_close to enable multiple
sockets close.
"""
for socketinfo in self._sockets:
socket_, addrinfo = socketinfo
logging.info('Close on: %r', addrinfo)
socket_.close()
def fileno(self):
"""Override SocketServer.TCPServer.fileno."""
logging.critical('Not supported: fileno')
return self._sockets[0][0].fileno()
def handle_error(self, rquest, client_address):
"""Override SocketServer.handle_error."""
@ -212,13 +316,48 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
# Note: client_address is a tuple. To match it against %r, we need the
# trailing comma.
def serve_forever(self, poll_interval=0.5):
"""Override SocketServer.BaseServer.serve_forever."""
self.__ws_serving = True
self.__ws_is_shut_down.clear()
handle_request = self.handle_request
if hasattr(self, '_handle_request_noblock'):
handle_request = self._handle_request_noblock
else:
logging.warning('mod_pywebsocket: fallback to blocking request '
'handler')
try:
while self.__ws_serving:
r, w, e = select.select(
[socket_[0] for socket_ in self._sockets],
[], [], poll_interval)
for socket_ in r:
self.socket = socket_
handle_request()
self.socket = None
finally:
self.__ws_is_shut_down.set()
def shutdown(self):
"""Override SocketServer.BaseServer.shutdown."""
self.__ws_serving = False
self.__ws_is_shut_down.wait()
class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
"""CGIHTTPRequestHandler specialized for WebSocket."""
def setup(self):
"""Override SocketServer.StreamRequestHandler.setup to wrap rfile with
MemorizingFile.
"""Override SocketServer.StreamRequestHandler.setup to wrap rfile
with MemorizingFile.
This method will be called by BaseRequestHandler's constructor
before calling BaseHTTPRequestHandler.handle.
BaseHTTPRequestHandler.handle will call
BaseHTTPRequestHandler.handle_one_request and it will call
WebSocketRequestHandler.parse_request.
"""
# Call superclass's setup to prepare rfile, wfile, etc. See setup
@ -230,55 +369,102 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
self.rfile,
max_memorized_lines=_MAX_MEMORIZED_LINES)
def __init__(self, *args, **keywords):
self._request = _StandaloneRequest(
self, WebSocketRequestHandler.options.use_tls)
self._dispatcher = WebSocketRequestHandler.options.dispatcher
self._print_warnings_if_any()
self._handshaker = handshake.Handshaker(
self._request, self._dispatcher,
allowDraft75=WebSocketRequestHandler.options.allow_draft75,
strict=WebSocketRequestHandler.options.strict)
CGIHTTPServer.CGIHTTPRequestHandler.__init__(
self, *args, **keywords)
def __init__(self, request, client_address, server):
self._options = server.websocket_server_options
def _print_warnings_if_any(self):
warnings = self._dispatcher.source_warnings()
if warnings:
for warning in warnings:
logging.warning('mod_pywebsocket: %s' % warning)
# Overrides CGIHTTPServerRequestHandler.cgi_directories.
self.cgi_directories = self._options.cgi_directories
# Replace CGIHTTPRequestHandler.is_executable method.
if self._options.is_executable_method is not None:
self.is_executable = self._options.is_executable_method
self._request = _StandaloneRequest(self, self._options.use_tls)
_print_warnings_if_any(self._options.dispatcher)
# This actually calls BaseRequestHandler.__init__.
CGIHTTPServer.CGIHTTPRequestHandler.__init__(
self, request, client_address, server)
def parse_request(self):
"""Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request.
Return True to continue processing for HTTP(S), False otherwise.
See BaseHTTPRequestHandler.handle_one_request method which calls
this method to understand how the return value will be handled.
"""
result = CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self)
if result:
try:
self._handshaker.do_handshake()
try:
self._dispatcher.transfer_data(self._request)
except Exception, e:
# Catch exception in transfer_data.
# In this case, handshake has been successful, so just log
# the exception and return False.
logging.info('mod_pywebsocket: %s' % e)
logging.info(
'mod_pywebsocket: %s' % util.get_stack_trace())
return False
except handshake.HandshakeError, e:
# Handshake for ws(s) failed. Assume http(s).
logging.info('mod_pywebsocket: %s' % e)
# We hook parse_request method, but also call the original
# CGIHTTPRequestHandler.parse_request since when we return False,
# CGIHTTPRequestHandler.handle_one_request continues processing and
# it needs variables set by CGIHTTPRequestHandler.parse_request.
#
# Variables set by this method will be also used by WebSocket request
# handling. See _StandaloneRequest.get_request, etc.
if not CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self):
return False
host, port, resource = http_header_util.parse_uri(self.path)
if resource is None:
logging.info('mod_pywebsocket: invalid uri %r' % self.path)
return True
server_options = self.server.websocket_server_options
if host is not None:
validation_host = server_options.validation_host
if validation_host is not None and host != validation_host:
logging.info('mod_pywebsocket: invalid host %r '
'(expected: %r)' % (host, validation_host))
return True
except dispatch.DispatchError, e:
if port is not None:
validation_port = server_options.validation_port
if validation_port is not None and port != validation_port:
logging.info('mod_pywebsocket: invalid port %r '
'(expected: %r)' % (port, validation_port))
return True
self.path = resource
try:
# Fallback to default http handler for request paths for which
# we don't have request handlers.
if not self._options.dispatcher.get_handler_suite(self.path):
logging.info('No handlers for request: %s' % self.path)
return True
try:
handshake.do_handshake(
self._request,
self._options.dispatcher,
allowDraft75=self._options.allow_draft75,
strict=self._options.strict)
except handshake.AbortedByUserException, e:
logging.info('mod_pywebsocket: %s' % e)
return False
try:
self._request._dispatcher = self._options.dispatcher
self._options.dispatcher.transfer_data(self._request)
except dispatch.DispatchException, e:
logging.warning('mod_pywebsocket: %s' % e)
return False
except handshake.AbortedByUserException, e:
logging.info('mod_pywebsocket: %s' % e)
except Exception, e:
logging.warning('mod_pywebsocket: %s' % e)
logging.warning('mod_pywebsocket: %s' % util.get_stack_trace())
return False
return result
# Catch exception in transfer_data.
# In this case, handshake has been successful, so just log
# the exception and return False.
logging.info('mod_pywebsocket: %s' % e)
logging.info(
'mod_pywebsocket: %s' % util.get_stack_trace())
except dispatch.DispatchException, e:
logging.warning('mod_pywebsocket: %s' % e)
self.send_error(e.status)
except handshake.HandshakeException, e:
# Handshake for ws(s) failed. Assume http(s).
logging.info('mod_pywebsocket: %s' % e)
self.send_error(e.status)
except Exception, e:
logging.warning('mod_pywebsocket: %s' % e)
logging.warning('mod_pywebsocket: %s' % util.get_stack_trace())
return False
def log_request(self, code='-', size='-'):
"""Override BaseHTTPServer.log_request."""
@ -291,7 +477,8 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
# Despite the name, this method is for warnings than for errors.
# For example, HTTP status code is logged by this method.
logging.warn('%s - %s' % (self.address_string(), (args[0] % args[1:])))
logging.warning('%s - %s' %
(self.address_string(), (args[0] % args[1:])))
def is_cgi(self):
"""Test whether self.path corresponds to a CGI script.
@ -301,6 +488,7 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
If the file is not executable, it is handled as static file or dir
rather than a CGI script.
"""
if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self):
if '..' in self.path:
return False
@ -337,6 +525,7 @@ def _alias_handlers(dispatcher, websock_handlers_map_file):
dispatcher: dispatch.Dispatcher instance
websock_handler_map_file: alias map file
"""
fp = open(websock_handlers_map_file)
try:
for line in fp:
@ -349,7 +538,7 @@ def _alias_handlers(dispatcher, websock_handlers_map_file):
try:
dispatcher.add_resource_path_alias(
m.group(1), m.group(2))
except dispatch.DispatchError, e:
except dispatch.DispatchException, e:
logging.error(str(e))
finally:
fp.close()
@ -361,9 +550,17 @@ def _main():
dest='server_host',
default='',
help='server hostname to listen to')
parser.add_option('-V', '--validation-host', '--validation_host',
dest='validation_host',
default=None,
help='server hostname to validate in absolute path.')
parser.add_option('-p', '--port', dest='port', type='int',
default=common.DEFAULT_WEB_SOCKET_PORT,
help='port to listen to')
parser.add_option('-P', '--validation-port', '--validation_port',
dest='validation_port', type='int',
default=None,
help='server port to validate in absolute path.')
parser.add_option('-w', '--websock-handlers', '--websock_handlers',
dest='websock_handlers',
default='.',
@ -422,12 +619,12 @@ def _main():
_configure_logging(options)
SocketServer.TCPServer.request_queue_size = options.request_queue_size
CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = []
# TODO(tyoshino): Clean up initialization of CGI related values. Move some
# of code here to WebSocketRequestHandler class if it's better.
options.cgi_directories = []
options.is_executable_method = None
if options.cgi_paths:
CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = \
options.cgi_paths.split(',')
options.cgi_directories = options.cgi_paths.split(',')
if sys.platform in ('cygwin', 'win32'):
cygwin_path = None
# For Win32 Python, it is expected that CYGWIN_PATH
@ -441,7 +638,7 @@ def _main():
def __check_script(scriptpath):
return util.get_script_interp(scriptpath, cygwin_path)
CGIHTTPServer.executable = __check_script
options.is_executable_method = __check_script
if options.use_tls:
if not _HAS_OPEN_SSL:
@ -465,11 +662,7 @@ def _main():
options.websock_handlers_map_file)
_print_warnings_if_any(options.dispatcher)
WebSocketRequestHandler.options = options
WebSocketServer.options = options
server = WebSocketServer((options.server_host, options.port),
WebSocketRequestHandler)
server = WebSocketServer(options)
server.serve_forever()
except Exception, e:
logging.critical('mod_pywebsocket: %s' % e)