зеркало из https://github.com/mozilla/pjs.git
Bug 689006: Upgrade pywebsocket to binary API (version 553, also supports Python > 2.5) r=mcmanus
This commit is contained in:
Родитель
3757351c07
Коммит
e2df9ca10b
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)))
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Загрузка…
Ссылка в новой задаче