зеркало из https://github.com/mozilla/gecko-dev.git
bug 640003 websockets - update incorporated pywebsockets to support -07 r=biesi
This commit is contained in:
Родитель
6ce0ea820e
Коммит
4ff34a4169
|
@ -108,18 +108,25 @@ _PYWEBSOCKET_FILES = \
|
|||
|
||||
_MOD_PYWEBSOCKET_FILES = \
|
||||
pywebsocket/mod_pywebsocket/__init__.py \
|
||||
pywebsocket/mod_pywebsocket/common.py \
|
||||
pywebsocket/mod_pywebsocket/dispatch.py \
|
||||
pywebsocket/mod_pywebsocket/util.py \
|
||||
pywebsocket/mod_pywebsocket/msgutil.py \
|
||||
pywebsocket/mod_pywebsocket/memorizingfile.py \
|
||||
pywebsocket/mod_pywebsocket/headerparserhandler.py \
|
||||
pywebsocket/mod_pywebsocket/memorizingfile.py \
|
||||
pywebsocket/mod_pywebsocket/util.py \
|
||||
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/standalone.py \
|
||||
pywebsocket/mod_pywebsocket/_stream_base.py \
|
||||
$(NULL)
|
||||
|
||||
_HANDSHAKE_FILES = \
|
||||
pywebsocket/mod_pywebsocket/handshake/__init__.py \
|
||||
pywebsocket/mod_pywebsocket/handshake/hybi00.py \
|
||||
pywebsocket/mod_pywebsocket/handshake/_base.py \
|
||||
pywebsocket/mod_pywebsocket/handshake/draft75.py \
|
||||
pywebsocket/mod_pywebsocket/handshake/handshake.py \
|
||||
pywebsocket/mod_pywebsocket/handshake/hybi06.py \
|
||||
$(NULL)
|
||||
|
||||
_DEST_DIR = $(DEPTH)/_tests/$(relativesrcdir)
|
||||
|
|
|
@ -1 +1,29 @@
|
|||
This is mod_pywebsocket 0.5, from http://code.google.com/p/pywebsocket/
|
||||
mod_pywebsocket http://pywebsocket.googlecode.com/svn
|
||||
version 470
|
||||
supporting ietf-07
|
||||
|
||||
includes the following minor patch::
|
||||
|
||||
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)
|
||||
|
|
|
@ -28,11 +28,12 @@
|
|||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Web Socket extension for Apache HTTP Server.
|
||||
"""WebSocket extension for Apache HTTP Server.
|
||||
|
||||
mod_pywebsocket is a Web Socket extension for Apache HTTP Server
|
||||
mod_pywebsocket is a WebSocket extension for Apache HTTP Server
|
||||
intended for testing or experimental purposes. mod_python is required.
|
||||
|
||||
|
||||
Installation:
|
||||
|
||||
0. Prepare an Apache HTTP Server for which mod_python is enabled.
|
||||
|
@ -46,27 +47,26 @@ Installation:
|
|||
PythonPath "sys.path+['<websock_lib>']"
|
||||
|
||||
Always specify the following. <websock_handlers> is the directory where
|
||||
user-written Web Socket handlers are placed.
|
||||
user-written WebSocket handlers are placed.
|
||||
|
||||
PythonOption mod_pywebsocket.handler_root <websock_handlers>
|
||||
PythonHeaderParserHandler mod_pywebsocket.headerparserhandler
|
||||
|
||||
To limit the search for Web Socket handlers to a directory <scan_dir>
|
||||
To limit the search for WebSocket handlers to a directory <scan_dir>
|
||||
under <websock_handlers>, configure as follows:
|
||||
|
||||
PythonOption mod_pywebsocket.handler_scan <scan_dir>
|
||||
|
||||
<scan_dir> is useful in saving scan time when <websock_handlers>
|
||||
contains many non-Web Socket handler files.
|
||||
contains many non-WebSocket handler files.
|
||||
|
||||
If you want to support old handshake based on
|
||||
draft-hixie-thewebsocketprotocol-75:
|
||||
|
||||
PythonOption mod_pywebsocket.allow_draft75 On
|
||||
|
||||
|
||||
Example snippet of httpd.conf:
|
||||
(mod_pywebsocket is in /websock_lib, Web Socket handlers are in
|
||||
(mod_pywebsocket is in /websock_lib, WebSocket handlers are in
|
||||
/websock_handlers, port is 80 for ws, 443 for wss.)
|
||||
|
||||
<IfModule python_module>
|
||||
|
@ -75,9 +75,17 @@ Installation:
|
|||
PythonHeaderParserHandler mod_pywebsocket.headerparserhandler
|
||||
</IfModule>
|
||||
|
||||
Writing Web Socket handlers:
|
||||
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.
|
||||
|
||||
When a Web Socket request comes in, the resource name
|
||||
3. Verify installation. You can use example/console.html to poke the server.
|
||||
|
||||
|
||||
Writing WebSocket handlers:
|
||||
|
||||
When a WebSocket request comes in, the resource name
|
||||
specified in the handshake is considered as if it is a file path under
|
||||
<websock_handlers> and the handler defined in
|
||||
<websock_handlers>/<resource_name>_wsh.py is invoked.
|
||||
|
@ -85,7 +93,7 @@ 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 Web Socket handler is composed of the following two functions:
|
||||
A WebSocket handler is composed of the following two functions:
|
||||
|
||||
web_socket_do_extra_handshake(request)
|
||||
web_socket_transfer_data(request)
|
||||
|
@ -94,16 +102,65 @@ where:
|
|||
request: mod_python request.
|
||||
|
||||
web_socket_do_extra_handshake is called during the handshake after the
|
||||
headers are successfully parsed and Web Socket properties (ws_location,
|
||||
ws_origin, ws_protocol, and ws_resource) are added to request. A handler
|
||||
headers are successfully parsed and WebSocket properties (ws_location,
|
||||
ws_origin, and ws_resource) are added to request. A handler
|
||||
can reject the request by raising an exception.
|
||||
|
||||
A request object has the following properties that you can use during the extra
|
||||
handshake (web_socket_do_extra_handshake):
|
||||
- ws_resource
|
||||
- ws_origin
|
||||
- ws_version
|
||||
- ws_location (Hixie 75 and HyBi 00 only)
|
||||
- ws_extensions (Hybi 06 and later)
|
||||
- ws_deflate (HyBi 06 and later)
|
||||
- ws_protocol
|
||||
- ws_requested_protocols (HyBi 06 and later)
|
||||
|
||||
The last two are a bit tricky.
|
||||
|
||||
For HyBi 06 and later, ws_protocol is always set to None when
|
||||
web_socket_do_extra_handshake is called. If ws_requested_protocols is not
|
||||
None, you must choose one subprotocol from this list and set it to ws_protocol.
|
||||
|
||||
For 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.
|
||||
|
||||
web_socket_transfer_data is called after the handshake completed
|
||||
successfully. A handler can receive/send messages from/to the client
|
||||
using request. mod_pywebsocket.msgutil module provides utilities
|
||||
for data transfer.
|
||||
|
||||
A Web Socket handler must be thread-safe if the server (Apache or
|
||||
You can receive a message by the following statement.
|
||||
|
||||
message = request.ws_stream.receive_message()
|
||||
|
||||
This call blocks until any complete text frame arrives, and the payload data of
|
||||
the incoming frame will be stored into message. When you're using IETF HyBi 00
|
||||
or later protocol, receive_message() will return None on receiving
|
||||
client-initiated closing handshake. When any error occurs, receive_message()
|
||||
will raise some exception.
|
||||
|
||||
You can send a message by the following statement.
|
||||
|
||||
request.ws_stream.send_message(message)
|
||||
|
||||
Executing the following statement or just return-ing from
|
||||
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.
|
||||
|
||||
A WebSocket handler must be thread-safe if the server (Apache or
|
||||
standalone.py) is configured to use threads.
|
||||
"""
|
||||
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Base stream class.
|
||||
"""
|
||||
|
||||
|
||||
# Note: request.connection.write/read are used in this module, even though
|
||||
# mod_python document says that they should be used only in connection
|
||||
# handlers. Unfortunately, we have no other options. For example,
|
||||
# request.write/read are not suitable because they don't allow direct raw bytes
|
||||
# writing/reading.
|
||||
|
||||
|
||||
from mod_pywebsocket import util
|
||||
|
||||
|
||||
# Exceptions
|
||||
class ConnectionTerminatedException(Exception):
|
||||
"""This exception will be raised when a connection is terminated
|
||||
unexpectedly.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidFrameException(ConnectionTerminatedException):
|
||||
"""This exception will be raised when we received an invalid frame we
|
||||
cannot parse.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BadOperationException(RuntimeError):
|
||||
"""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):
|
||||
"""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 StreamBase(object):
|
||||
"""Base stream class."""
|
||||
|
||||
def __init__(self, request):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
"""
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._request = request
|
||||
|
||||
def _read(self, length):
|
||||
"""Reads length bytes from connection. In case we catch any exception,
|
||||
prepends remote address to the exception message and raise again.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty string.
|
||||
"""
|
||||
|
||||
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,)))
|
||||
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:
|
||||
self._request.connection.write(bytes)
|
||||
except Exception, e:
|
||||
util.prepend_message_to_exception(
|
||||
'Failed to send message to %r: ' %
|
||||
(self._request.connection.remote_addr,),
|
||||
e)
|
||||
raise
|
||||
|
||||
def receive_bytes(self, length):
|
||||
"""Receives multiple bytes. Retries read when we couldn't receive the
|
||||
specified amount.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty string.
|
||||
"""
|
||||
|
||||
bytes = []
|
||||
while length > 0:
|
||||
new_bytes = self._read(length)
|
||||
bytes.append(new_bytes)
|
||||
length -= len(new_bytes)
|
||||
return ''.join(bytes)
|
||||
|
||||
def _read_until(self, delim_char):
|
||||
"""Reads bytes until we encounter delim_char. The result will not
|
||||
contain delim_char.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty string.
|
||||
"""
|
||||
|
||||
bytes = []
|
||||
while True:
|
||||
ch = self._read(1)
|
||||
if ch == delim_char:
|
||||
break
|
||||
bytes.append(ch)
|
||||
return ''.join(bytes)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -0,0 +1,218 @@
|
|||
# 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.
|
||||
|
||||
|
||||
"""Stream of WebSocket protocol with the framing used by IETF HyBi 00 and
|
||||
Hixie 75. For Hixie 75 this stream doesn't perform closing handshake.
|
||||
"""
|
||||
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket._stream_base import BadOperationException
|
||||
from mod_pywebsocket._stream_base import ConnectionTerminatedException
|
||||
from mod_pywebsocket._stream_base import InvalidFrameException
|
||||
from mod_pywebsocket._stream_base import StreamBase
|
||||
from mod_pywebsocket._stream_base import UnsupportedFrameException
|
||||
from mod_pywebsocket import util
|
||||
|
||||
|
||||
class StreamHixie75(StreamBase):
|
||||
"""Stream of WebSocket messages."""
|
||||
|
||||
def __init__(self, request, enable_closing_handshake=False):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
enable_closing_handshake: to let StreamHixie75 perform closing
|
||||
handshake as specified in HyBi 00, set
|
||||
this option to True.
|
||||
"""
|
||||
|
||||
StreamBase.__init__(self, request)
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._enable_closing_handshake = enable_closing_handshake
|
||||
|
||||
self._request.client_terminated = False
|
||||
self._request.server_terminated = False
|
||||
|
||||
def send_message(self, message, end=True):
|
||||
"""Send message.
|
||||
|
||||
Args:
|
||||
message: unicode string to send.
|
||||
|
||||
Raises:
|
||||
BadOperationException: when called on a server-terminated
|
||||
connection.
|
||||
"""
|
||||
|
||||
if not end:
|
||||
raise BadOperationException(
|
||||
'StreamHixie75 doesn\'t support send_message with end=False')
|
||||
|
||||
if self._request.server_terminated:
|
||||
raise BadOperationException(
|
||||
'Requested send_message after sending out a closing handshake')
|
||||
|
||||
self._write(''.join(['\x00', message.encode('utf-8'), '\xff']))
|
||||
|
||||
def _read_payload_length_hixie75(self):
|
||||
"""Reads a length header in a Hixie75 version frame with length.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty string.
|
||||
"""
|
||||
|
||||
length = 0
|
||||
while True:
|
||||
b_str = self._read(1)
|
||||
b = ord(b_str)
|
||||
length = length * 128 + (b & 0x7f)
|
||||
if (b & 0x80) == 0:
|
||||
break
|
||||
return length
|
||||
|
||||
def receive_message(self):
|
||||
"""Receive a WebSocket frame and return its payload an unicode string.
|
||||
|
||||
Returns:
|
||||
payload unicode string in a WebSocket frame.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty
|
||||
string.
|
||||
BadOperationException: when called on a client-terminated
|
||||
connection.
|
||||
"""
|
||||
|
||||
if self._request.client_terminated:
|
||||
raise BadOperationException(
|
||||
'Requested receive_message after receiving a closing '
|
||||
'handshake')
|
||||
|
||||
while True:
|
||||
# Read 1 byte.
|
||||
# mp_conn.read will block if no bytes are available.
|
||||
# Timeout is controlled by TimeOut directive of Apache.
|
||||
frame_type_str = self.receive_bytes(1)
|
||||
frame_type = ord(frame_type_str)
|
||||
if (frame_type & 0x80) == 0x80:
|
||||
# The payload length is specified in the frame.
|
||||
# Read and discard.
|
||||
length = self._read_payload_length_hixie75()
|
||||
if length > 0:
|
||||
_ = self.receive_bytes(length)
|
||||
# 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the
|
||||
# /client terminated/ flag and abort these steps.
|
||||
if not self._enable_closing_handshake:
|
||||
continue
|
||||
|
||||
if frame_type == 0xFF and length == 0:
|
||||
self._request.client_terminated = True
|
||||
|
||||
if self._request.server_terminated:
|
||||
self._logger.debug(
|
||||
'Received ack for server-initiated closing '
|
||||
'handshake')
|
||||
return None
|
||||
|
||||
self._logger.debug(
|
||||
'Received client-initiated closing handshake')
|
||||
|
||||
self._send_closing_handshake()
|
||||
self._logger.debug(
|
||||
'Sent ack for client-initiated closing handshake')
|
||||
return None
|
||||
else:
|
||||
# The payload is delimited with \xff.
|
||||
bytes = self._read_until('\xff')
|
||||
# The WebSocket protocol section 4.4 specifies that invalid
|
||||
# characters must be replaced with U+fffd REPLACEMENT
|
||||
# CHARACTER.
|
||||
message = bytes.decode('utf-8', 'replace')
|
||||
if frame_type == 0x00:
|
||||
return message
|
||||
# Discard data of other types.
|
||||
|
||||
def _send_closing_handshake(self):
|
||||
if not self._enable_closing_handshake:
|
||||
raise BadOperationException(
|
||||
'Closing handshake is not supported in Hixie 75 protocol')
|
||||
|
||||
self._request.server_terminated = True
|
||||
|
||||
# 5.3 the server may decide to terminate the WebSocket connection by
|
||||
# running through the following steps:
|
||||
# 1. send a 0xFF byte and a 0x00 byte to the client to indicate the
|
||||
# start of the closing handshake.
|
||||
self._write('\xff\x00')
|
||||
|
||||
def close_connection(self, unused_code='', unused_reason=''):
|
||||
"""Closes a WebSocket connection.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when closing handshake was
|
||||
not successfull.
|
||||
"""
|
||||
|
||||
if self._request.server_terminated:
|
||||
self._logger.debug(
|
||||
'Requested close_connection but server is already terminated')
|
||||
return
|
||||
|
||||
if not self._enable_closing_handshake:
|
||||
self._request.server_terminated = True
|
||||
self._logger.debug('Connection closed')
|
||||
return
|
||||
|
||||
self._send_closing_handshake()
|
||||
self._logger.debug('Sent server-initiated closing handshake')
|
||||
|
||||
# TODO(ukai): 2. wait until the /client terminated/ flag has been set,
|
||||
# or until a server-defined timeout expires.
|
||||
#
|
||||
# For now, we expect receiving closing handshake right after sending
|
||||
# out closing handshake, and if we couldn't receive non-handshake
|
||||
# frame, we take it as ConnectionTerminatedException.
|
||||
message = self.receive_message()
|
||||
if message is not None:
|
||||
raise ConnectionTerminatedException(
|
||||
'Didn\'t receive valid ack for closing handshake')
|
||||
# TODO: 3. close the WebSocket connection.
|
||||
# note: mod_python Connection (mp_conn) doesn't have close method.
|
||||
|
||||
def send_ping(self, body):
|
||||
raise BadOperationException(
|
||||
'StreamHixie75 doesn\'t support send_ping')
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -0,0 +1,510 @@
|
|||
# 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.
|
||||
|
||||
|
||||
"""Stream class for IETF HyBi 07 WebSocket protocol.
|
||||
"""
|
||||
|
||||
|
||||
from collections import deque
|
||||
import os
|
||||
import struct
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket import util
|
||||
from mod_pywebsocket._stream_base import BadOperationException
|
||||
from mod_pywebsocket._stream_base import ConnectionTerminatedException
|
||||
from mod_pywebsocket._stream_base import InvalidFrameException
|
||||
from mod_pywebsocket._stream_base import StreamBase
|
||||
from mod_pywebsocket._stream_base import UnsupportedFrameException
|
||||
|
||||
|
||||
def is_control_opcode(opcode):
|
||||
return (opcode >> 3) == 1
|
||||
|
||||
|
||||
_NOOP_MASKER = util.NoopMasker()
|
||||
|
||||
|
||||
# Helper functions made public to be used for writing unittests for WebSocket
|
||||
# clients.
|
||||
def create_length_header(length, mask):
|
||||
"""Creates a length header.
|
||||
|
||||
Args:
|
||||
length: Frame length. Must be less than 2^63.
|
||||
mask: Mask bit. Must be boolean.
|
||||
|
||||
Raises:
|
||||
ValueError: when bad data is given.
|
||||
"""
|
||||
|
||||
if mask:
|
||||
mask_bit = 1 << 7
|
||||
else:
|
||||
mask_bit = 0
|
||||
|
||||
if length < 0:
|
||||
raise ValueError('length must be non negative integer')
|
||||
elif length <= 125:
|
||||
return chr(mask_bit | length)
|
||||
elif length < (1 << 16):
|
||||
return chr(mask_bit | 126) + struct.pack('!H', length)
|
||||
elif length < (1 << 63):
|
||||
return chr(mask_bit | 127) + struct.pack('!Q', length)
|
||||
else:
|
||||
raise ValueError('Payload is too big for one frame')
|
||||
|
||||
|
||||
def create_header(opcode, payload_length, fin, rsv1, rsv2, rsv3, mask):
|
||||
"""Creates a frame header.
|
||||
|
||||
Raises:
|
||||
Exception: when bad data is given.
|
||||
"""
|
||||
|
||||
if opcode < 0 or 0xf < opcode:
|
||||
raise ValueError('Opcode out of range')
|
||||
|
||||
if payload_length < 0 or (1 << 63) <= payload_length:
|
||||
raise ValueError('payload_length out of range')
|
||||
|
||||
if (fin | rsv1 | rsv2 | rsv3) & ~1:
|
||||
raise ValueError('FIN bit and Reserved bit parameter must be 0 or 1')
|
||||
|
||||
header = ''
|
||||
|
||||
first_byte = ((fin << 7)
|
||||
| (rsv1 << 6) | (rsv2 << 5) | (rsv3 << 4)
|
||||
| opcode)
|
||||
header += chr(first_byte)
|
||||
header += create_length_header(payload_length, mask)
|
||||
|
||||
return header
|
||||
|
||||
|
||||
def _build_frame(header, body, mask):
|
||||
if not mask:
|
||||
return header + body
|
||||
|
||||
masking_nonce = os.urandom(4)
|
||||
masker = util.RepeatedXorMasker(masking_nonce)
|
||||
|
||||
return header + masking_nonce + masker.mask(body)
|
||||
|
||||
|
||||
def create_text_frame(message, opcode=common.OPCODE_TEXT, fin=1, mask=False):
|
||||
"""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)
|
||||
|
||||
|
||||
class FragmentedTextFrameBuilder(object):
|
||||
"""A stateful class to send a message as fragments."""
|
||||
|
||||
def __init__(self, mask):
|
||||
"""Constructs an instance."""
|
||||
|
||||
self._mask = mask
|
||||
|
||||
self._started = False
|
||||
|
||||
def build(self, message, end):
|
||||
if self._started:
|
||||
opcode = common.OPCODE_CONTINUATION
|
||||
else:
|
||||
opcode = common.OPCODE_TEXT
|
||||
|
||||
if end:
|
||||
self._started = False
|
||||
fin = 1
|
||||
else:
|
||||
self._started = True
|
||||
fin = 0
|
||||
|
||||
return create_text_frame(message, opcode, fin, self._mask)
|
||||
|
||||
|
||||
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_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_close_frame(body, mask=False):
|
||||
header = create_header(common.OPCODE_CLOSE, len(body), 1, 0, 0, 0, mask)
|
||||
return _build_frame(header, body, mask)
|
||||
|
||||
|
||||
class StreamOptions(object):
|
||||
def __init__(self):
|
||||
self.deflate = False
|
||||
self.mask_send = False
|
||||
self.unmask_receive = True
|
||||
|
||||
|
||||
class Stream(StreamBase):
|
||||
"""Stream of WebSocket messages."""
|
||||
|
||||
def __init__(self, request, options):
|
||||
"""Constructs an instance.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
"""
|
||||
|
||||
StreamBase.__init__(self, request)
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._options = options
|
||||
|
||||
if self._options.deflate:
|
||||
self._logger.debug('Deflated stream')
|
||||
self._request = util.DeflateRequest(self._request)
|
||||
|
||||
self._request.client_terminated = False
|
||||
self._request.server_terminated = False
|
||||
|
||||
# Holds body of received fragments.
|
||||
self._received_fragments = []
|
||||
# Holds the opcode of the first fragment.
|
||||
self._original_opcode = None
|
||||
|
||||
self._writer = FragmentedTextFrameBuilder(self._options.mask_send)
|
||||
|
||||
self._ping_queue = deque()
|
||||
|
||||
def _receive_frame(self):
|
||||
"""Receives a frame and return data in the frame as a tuple containing
|
||||
each header field and payload separately.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty
|
||||
string.
|
||||
InvalidFrameException: when the frame contains invalid data.
|
||||
"""
|
||||
|
||||
received = self.receive_bytes(2)
|
||||
|
||||
first_byte = ord(received[0])
|
||||
fin = (first_byte >> 7) & 1
|
||||
rsv1 = (first_byte >> 6) & 1
|
||||
rsv2 = (first_byte >> 5) & 1
|
||||
rsv3 = (first_byte >> 4) & 1
|
||||
opcode = first_byte & 0xf
|
||||
|
||||
second_byte = ord(received[1])
|
||||
mask = (second_byte >> 7) & 1
|
||||
payload_length = second_byte & 0x7f
|
||||
|
||||
if (mask == 1) != self._options.unmask_receive:
|
||||
raise InvalidFrameException(
|
||||
'Mask bit on the received frame did\'nt match masking '
|
||||
'configuration for received frames')
|
||||
|
||||
if payload_length == 127:
|
||||
extended_payload_length = self.receive_bytes(8)
|
||||
payload_length = struct.unpack(
|
||||
'!Q', extended_payload_length)[0]
|
||||
if payload_length > 0x7FFFFFFFFFFFFFFF:
|
||||
raise InvalidFrameException(
|
||||
'Extended payload length >= 2^63')
|
||||
elif payload_length == 126:
|
||||
extended_payload_length = self.receive_bytes(2)
|
||||
payload_length = struct.unpack(
|
||||
'!H', extended_payload_length)[0]
|
||||
|
||||
if mask == 1:
|
||||
masking_nonce = self.receive_bytes(4)
|
||||
masker = util.RepeatedXorMasker(masking_nonce)
|
||||
else:
|
||||
masker = _NOOP_MASKER
|
||||
|
||||
bytes = masker.mask(self.receive_bytes(payload_length))
|
||||
|
||||
return opcode, bytes, fin, rsv1, rsv2, rsv3
|
||||
|
||||
def send_message(self, message, end=True):
|
||||
"""Send message.
|
||||
|
||||
Args:
|
||||
message: unicode string to send.
|
||||
|
||||
Raises:
|
||||
BadOperationException: when called on a server-terminated
|
||||
connection.
|
||||
"""
|
||||
|
||||
if self._request.server_terminated:
|
||||
raise BadOperationException(
|
||||
'Requested send_message after sending out a closing handshake')
|
||||
|
||||
self._write(self._writer.build(message, end))
|
||||
|
||||
def receive_message(self):
|
||||
"""Receive a WebSocket frame and return its payload an unicode string.
|
||||
|
||||
Returns:
|
||||
payload unicode string in a WebSocket frame. None iff received
|
||||
closing handshake.
|
||||
Raises:
|
||||
BadOperationException: when called on a client-terminated
|
||||
connection.
|
||||
ConnectionTerminatedException: when read returns empty
|
||||
string.
|
||||
InvalidFrameException: when the frame contains invalid
|
||||
data.
|
||||
UnsupportedFrameException: when the received frame has
|
||||
flags, opcode we cannot handle. You can ignore this exception
|
||||
and continue receiving the next frame.
|
||||
"""
|
||||
|
||||
if self._request.client_terminated:
|
||||
raise BadOperationException(
|
||||
'Requested receive_message after receiving a closing handshake')
|
||||
|
||||
while True:
|
||||
# mp_conn.read will block if no bytes are available.
|
||||
# Timeout is controlled by TimeOut directive of Apache.
|
||||
|
||||
opcode, bytes, fin, rsv1, rsv2, rsv3 = self._receive_frame()
|
||||
if rsv1 or rsv2 or rsv3:
|
||||
raise UnsupportedFrameException(
|
||||
'Unsupported flag is set (rsv = %d%d%d)' %
|
||||
(rsv1, rsv2, rsv3))
|
||||
|
||||
if opcode == common.OPCODE_CONTINUATION:
|
||||
if not self._received_fragments:
|
||||
if fin:
|
||||
raise InvalidFrameException(
|
||||
'Received a termination frame but fragmentation '
|
||||
'not started')
|
||||
else:
|
||||
raise InvalidFrameException(
|
||||
'Received an intermediate frame but '
|
||||
'fragmentation not started')
|
||||
|
||||
if fin:
|
||||
# End of fragmentation frame
|
||||
self._received_fragments.append(bytes)
|
||||
message = ''.join(self._received_fragments)
|
||||
self._received_fragments = []
|
||||
else:
|
||||
# Intermediate frame
|
||||
self._received_fragments.append(bytes)
|
||||
continue
|
||||
else:
|
||||
if self._received_fragments:
|
||||
if fin:
|
||||
raise InvalidFrameException(
|
||||
'Received an unfragmented frame without '
|
||||
'terminating existing fragmentation')
|
||||
else:
|
||||
raise InvalidFrameException(
|
||||
'New fragmentation started without terminating '
|
||||
'existing fragmentation')
|
||||
|
||||
if fin:
|
||||
# Unfragmented frame
|
||||
self._original_opcode = opcode
|
||||
message = bytes
|
||||
|
||||
if is_control_opcode(opcode) and len(message) > 125:
|
||||
raise InvalidFrameException(
|
||||
'Application data size of control frames must be '
|
||||
'125 bytes or less')
|
||||
else:
|
||||
# Start of fragmentation frame
|
||||
|
||||
if is_control_opcode(opcode):
|
||||
raise InvalidFrameException(
|
||||
'Control frames must not be fragmented')
|
||||
|
||||
self._original_opcode = opcode
|
||||
self._received_fragments.append(bytes)
|
||||
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')
|
||||
elif self._original_opcode == common.OPCODE_CLOSE:
|
||||
self._request.client_terminated = True
|
||||
|
||||
# Status code is optional. We can have status reason only if we
|
||||
# have status code. Status reason can be empty string. So,
|
||||
# allowed cases are
|
||||
# - no application data: no code no reason
|
||||
# - 2 octet of application data: has code but no reason
|
||||
# - 3 or more octet of application data: both code and reason
|
||||
if len(message) == 1:
|
||||
raise InvalidFrameException(
|
||||
'If a close frame has status code, the length of '
|
||||
'status code must be 2 octet')
|
||||
elif len(message) >= 2:
|
||||
self._request.ws_close_code = struct.unpack(
|
||||
'!H', message[0:2])[0]
|
||||
self._request.ws_close_reason = message[2:].decode(
|
||||
'utf-8', 'replace')
|
||||
|
||||
if self._request.server_terminated:
|
||||
self._logger.debug(
|
||||
'Received ack for server-initiated closing '
|
||||
'handshake')
|
||||
return None
|
||||
|
||||
self._logger.debug(
|
||||
'Received client-initiated closing handshake')
|
||||
|
||||
self._send_closing_handshake(common.STATUS_NORMAL, '')
|
||||
self._logger.debug(
|
||||
'Sent ack for client-initiated closing handshake')
|
||||
return None
|
||||
elif self._original_opcode == common.OPCODE_PING:
|
||||
try:
|
||||
handler = self._request.on_ping_handler
|
||||
if handler:
|
||||
handler(self._request, message)
|
||||
continue
|
||||
except AttributeError, e:
|
||||
pass
|
||||
self._send_pong(message)
|
||||
elif self._original_opcode == common.OPCODE_PONG:
|
||||
# TODO(tyoshino): Add ping timeout handling.
|
||||
|
||||
inflight_pings = deque()
|
||||
|
||||
while True:
|
||||
try:
|
||||
expected_body = self._ping_queue.popleft()
|
||||
if expected_body == message:
|
||||
# inflight_pings contains pings ignored by the
|
||||
# other peer. Just forget them.
|
||||
self._logger.debug(
|
||||
'Ping %r is acked (%d pings were ignored)' %
|
||||
(expected_body, len(inflight_pings)))
|
||||
break
|
||||
else:
|
||||
inflight_pings.append(expected_body)
|
||||
except IndexError, e:
|
||||
# The received pong was unsolicited pong. Keep the
|
||||
# ping queue as is.
|
||||
self._ping_queue = inflight_pings
|
||||
self._logger.debug('Received a unsolicited pong')
|
||||
break
|
||||
|
||||
try:
|
||||
handler = self._request.on_pong_handler
|
||||
if handler:
|
||||
handler(self._request, message)
|
||||
continue
|
||||
except AttributeError, e:
|
||||
pass
|
||||
|
||||
continue
|
||||
else:
|
||||
raise UnsupportedFrameException(
|
||||
'Opcode %d is not supported' % self._original_opcode)
|
||||
|
||||
def _send_closing_handshake(self, code, reason):
|
||||
if code >= (1 << 16) or code < 0:
|
||||
raise BadOperationException('Status code is out of range')
|
||||
|
||||
encoded_reason = reason.encode('utf-8')
|
||||
if len(encoded_reason) + 2 > 125:
|
||||
raise BadOperationException(
|
||||
'Application data size of close frames must be 125 bytes or '
|
||||
'less')
|
||||
|
||||
frame = create_close_frame(
|
||||
struct.pack('!H', code) + encoded_reason, self._options.mask_send)
|
||||
|
||||
self._request.server_terminated = True
|
||||
|
||||
self._write(frame)
|
||||
|
||||
def close_connection(self, code=common.STATUS_NORMAL, reason=''):
|
||||
"""Closes a WebSocket connection."""
|
||||
|
||||
if self._request.server_terminated:
|
||||
self._logger.debug(
|
||||
'Requested close_connection but server is already terminated')
|
||||
return
|
||||
|
||||
self._send_closing_handshake(code, reason)
|
||||
self._logger.debug('Sent server-initiated closing handshake')
|
||||
|
||||
if (code == common.STATUS_GOING_AWAY or
|
||||
code == common.STATUS_PROTOCOL_ERROR):
|
||||
# It doesn't make sense to wait for a close frame if the reason is
|
||||
# protocol error or that the server is going away. For some of other
|
||||
# reasons, it might not make sense to wait for a close frame, but
|
||||
# it's not clear, yet.
|
||||
return
|
||||
|
||||
# TODO(ukai): 2. wait until the /client terminated/ flag has been set,
|
||||
# or until a server-defined timeout expires.
|
||||
#
|
||||
# For now, we expect receiving closing handshake right after sending
|
||||
# out closing handshake.
|
||||
message = self.receive_message()
|
||||
if message is not None:
|
||||
raise ConnectionTerminatedException(
|
||||
'Didn\'t receive valid ack for closing handshake')
|
||||
# TODO: 3. close the WebSocket connection.
|
||||
# note: mod_python Connection (mp_conn) doesn't have close method.
|
||||
|
||||
def send_ping(self, body=''):
|
||||
if len(body) > 125:
|
||||
raise ValueError(
|
||||
'Application data size of control frames must be 125 bytes or '
|
||||
'less')
|
||||
frame = create_ping_frame(body, self._options.mask_send)
|
||||
self._write(frame)
|
||||
|
||||
self._ping_queue.append(body)
|
||||
|
||||
def _send_pong(self, body):
|
||||
if len(body) > 125:
|
||||
raise ValueError(
|
||||
'Application data size of control frames must be 125 bytes or '
|
||||
'less')
|
||||
frame = create_pong_frame(body, self._options.mask_send)
|
||||
self._write(frame)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -0,0 +1,74 @@
|
|||
# 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.
|
||||
|
||||
|
||||
# Constants indicating WebSocket protocol version.
|
||||
VERSION_HYBI07 = 7
|
||||
VERSION_HYBI00 = 0
|
||||
VERSION_HIXIE75 = -1
|
||||
|
||||
# Port numbers
|
||||
DEFAULT_WEB_SOCKET_PORT = 80
|
||||
DEFAULT_WEB_SOCKET_SECURE_PORT = 443
|
||||
|
||||
# Schemes
|
||||
WEB_SOCKET_SCHEME = 'ws'
|
||||
WEB_SOCKET_SECURE_SCHEME = 'wss'
|
||||
|
||||
# Frame opcodes defined in the spec.
|
||||
OPCODE_CONTINUATION = 0x0
|
||||
OPCODE_TEXT = 0x1
|
||||
OPCODE_BINARY = 0x2
|
||||
OPCODE_CLOSE = 0x8
|
||||
OPCODE_PING = 0x9
|
||||
OPCODE_PONG = 0xa
|
||||
|
||||
# UUIDs used by HyBi 07 opening handshake and frame masking.
|
||||
WEBSOCKET_ACCEPT_UUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
||||
|
||||
# Opening handshake header names and expected values.
|
||||
UPGRADE_HEADER = 'Upgrade'
|
||||
WEBSOCKET_UPGRADE_TYPE = 'websocket'
|
||||
WEBSOCKET_UPGRADE_TYPE_HIXIE75 = 'WebSocket'
|
||||
CONNECTION_HEADER = 'Connection'
|
||||
UPGRADE_CONNECTION_TYPE = 'Upgrade'
|
||||
HOST_HEADER = 'Host'
|
||||
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'
|
||||
|
||||
# Status codes
|
||||
STATUS_NORMAL = 1000
|
||||
STATUS_GOING_AWAY = 1001
|
||||
STATUS_PROTOCOL_ERROR = 1002
|
||||
STATUS_UNSUPPORTED = 1003
|
||||
STATUS_TOO_LARGE = 1004
|
|
@ -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
|
||||
|
@ -28,13 +28,15 @@
|
|||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Dispatch Web Socket request.
|
||||
"""Dispatch WebSocket request.
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket import msgutil
|
||||
from mod_pywebsocket import util
|
||||
|
||||
|
@ -46,7 +48,7 @@ _TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data'
|
|||
|
||||
|
||||
class DispatchError(Exception):
|
||||
"""Exception in dispatching Web Socket request."""
|
||||
"""Exception in dispatching WebSocket request."""
|
||||
|
||||
pass
|
||||
|
||||
|
@ -63,15 +65,18 @@ def _normalize_path(path):
|
|||
"""
|
||||
|
||||
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 _path_to_resource_converter(base_dir):
|
||||
def _create_path_to_resource_converter(base_dir):
|
||||
base_dir = _normalize_path(base_dir)
|
||||
|
||||
base_len = len(base_dir)
|
||||
suffix_len = len(_SOURCE_SUFFIX)
|
||||
|
||||
def converter(path):
|
||||
if not path.endswith(_SOURCE_SUFFIX):
|
||||
return None
|
||||
|
@ -79,11 +84,14 @@ def _path_to_resource_converter(base_dir):
|
|||
if not path.startswith(base_dir):
|
||||
return None
|
||||
return path[base_len:-suffix_len]
|
||||
|
||||
return converter
|
||||
|
||||
|
||||
def _source_file_paths(directory):
|
||||
"""Yield Web Socket Handler source file names in the given directory."""
|
||||
def _enumerate_handler_file_paths(directory):
|
||||
"""Returns a generator that enumerates WebSocket Handler source file names
|
||||
in the given directory.
|
||||
"""
|
||||
|
||||
for root, unused_dirs, files in os.walk(directory):
|
||||
for base in files:
|
||||
|
@ -92,20 +100,38 @@ def _source_file_paths(directory):
|
|||
yield path
|
||||
|
||||
|
||||
def _source(source_str):
|
||||
"""Source a handler definition string."""
|
||||
class _HandlerSuite(object):
|
||||
"""A handler suite holder class."""
|
||||
|
||||
def __init__(self, do_extra_handshake, transfer_data):
|
||||
self.do_extra_handshake = do_extra_handshake
|
||||
self.transfer_data = transfer_data
|
||||
|
||||
|
||||
def _source_handler_file(handler_definition):
|
||||
"""Source a handler definition string.
|
||||
|
||||
Args:
|
||||
handler_definition: a string containing Python statements that define
|
||||
handler functions.
|
||||
"""
|
||||
|
||||
global_dic = {}
|
||||
try:
|
||||
exec source_str in global_dic
|
||||
exec handler_definition in global_dic
|
||||
except Exception:
|
||||
raise DispatchError('Error in sourcing handler:' +
|
||||
util.get_stack_trace())
|
||||
return (_extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME),
|
||||
_extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME))
|
||||
return _HandlerSuite(
|
||||
_extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME),
|
||||
_extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME))
|
||||
|
||||
|
||||
def _extract_handler(dic, name):
|
||||
"""Extracts a callable with the specified name from the given dictionary
|
||||
dic.
|
||||
"""
|
||||
|
||||
if name not in dic:
|
||||
raise DispatchError('%s is not defined.' % name)
|
||||
handler = dic[name]
|
||||
|
@ -115,7 +141,7 @@ def _extract_handler(dic, name):
|
|||
|
||||
|
||||
class Dispatcher(object):
|
||||
"""Dispatches Web Socket requests.
|
||||
"""Dispatches WebSocket requests.
|
||||
|
||||
This class maintains a map from resource name to handlers.
|
||||
"""
|
||||
|
@ -133,7 +159,9 @@ class Dispatcher(object):
|
|||
scan time when root_dir contains many subdirectories.
|
||||
"""
|
||||
|
||||
self._handlers = {}
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._handler_suite_map = {}
|
||||
self._source_warnings = []
|
||||
if scan_dir is None:
|
||||
scan_dir = root_dir
|
||||
|
@ -141,7 +169,7 @@ class Dispatcher(object):
|
|||
os.path.realpath(root_dir)):
|
||||
raise DispatchError('scan_dir:%s must be a directory under '
|
||||
'root_dir:%s.' % (scan_dir, root_dir))
|
||||
self._source_files_in_dir(root_dir, scan_dir)
|
||||
self._source_handler_files_in_dir(root_dir, scan_dir)
|
||||
|
||||
def add_resource_path_alias(self,
|
||||
alias_resource_path, existing_resource_path):
|
||||
|
@ -155,8 +183,8 @@ class Dispatcher(object):
|
|||
existing_resource_path: existing resource path
|
||||
"""
|
||||
try:
|
||||
handler = self._handlers[existing_resource_path]
|
||||
self._handlers[alias_resource_path] = handler
|
||||
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)
|
||||
|
||||
|
@ -166,7 +194,7 @@ class Dispatcher(object):
|
|||
return self._source_warnings
|
||||
|
||||
def do_extra_handshake(self, request):
|
||||
"""Do extra checking in Web Socket handshake.
|
||||
"""Do extra checking in WebSocket handshake.
|
||||
|
||||
Select a handler based on request.uri and call its
|
||||
web_socket_do_extra_handshake function.
|
||||
|
@ -175,7 +203,8 @@ class Dispatcher(object):
|
|||
request: mod_python request.
|
||||
"""
|
||||
|
||||
do_extra_handshake_, unused_transfer_data = self._handler(request)
|
||||
do_extra_handshake_ = self._get_handler_suite(
|
||||
request).do_extra_handshake
|
||||
try:
|
||||
do_extra_handshake_(request)
|
||||
except Exception, e:
|
||||
|
@ -187,7 +216,7 @@ class Dispatcher(object):
|
|||
raise
|
||||
|
||||
def transfer_data(self, request):
|
||||
"""Let a handler transfer_data with a Web Socket client.
|
||||
"""Let a handler transfer_data with a WebSocket client.
|
||||
|
||||
Select a handler based on request.ws_resource and call its
|
||||
web_socket_transfer_data function.
|
||||
|
@ -196,50 +225,58 @@ class Dispatcher(object):
|
|||
request: mod_python request.
|
||||
"""
|
||||
|
||||
unused_do_extra_handshake, transfer_data_ = self._handler(request)
|
||||
transfer_data_ = self._get_handler_suite(request).transfer_data
|
||||
# TODO(tyoshino): Terminate underlying TCP connection if possible.
|
||||
try:
|
||||
try:
|
||||
request.client_terminated = False
|
||||
request.server_terminated = False
|
||||
transfer_data_(request)
|
||||
except msgutil.ConnectionTerminatedException, e:
|
||||
util.prepend_message_to_exception(
|
||||
'client initiated closing handshake for %s: ' % (
|
||||
request.ws_resource),
|
||||
e)
|
||||
raise
|
||||
except Exception, e:
|
||||
print 'exception: %s' % type(e)
|
||||
util.prepend_message_to_exception(
|
||||
'%s raised exception for %s: ' % (
|
||||
transfer_data_(request)
|
||||
if not request.server_terminated:
|
||||
request.ws_stream.close_connection()
|
||||
# Catch non-critical exceptions the handler didn't handle.
|
||||
except msgutil.BadOperationException, e:
|
||||
self._logger.debug(str(e))
|
||||
request.ws_stream.close_connection(common.STATUS_GOING_AWAY)
|
||||
except msgutil.InvalidFrameException, e:
|
||||
# InvalidFrameException must be caught before
|
||||
# ConnectionTerminatedException that catches InvalidFrameException.
|
||||
self._logger.debug(str(e))
|
||||
request.ws_stream.close_connection(common.STATUS_PROTOCOL_ERROR)
|
||||
except msgutil.UnsupportedFrameException, e:
|
||||
self._logger.debug(str(e))
|
||||
request.ws_stream.close_connection(common.STATUS_UNSUPPORTED)
|
||||
except msgutil.ConnectionTerminatedException, e:
|
||||
self._logger.debug(str(e))
|
||||
except Exception, e:
|
||||
util.prepend_message_to_exception(
|
||||
'%s raised exception for %s: ' % (
|
||||
_TRANSFER_DATA_HANDLER_NAME, request.ws_resource),
|
||||
e)
|
||||
raise
|
||||
finally:
|
||||
msgutil.close_connection(request)
|
||||
e)
|
||||
raise
|
||||
|
||||
def _get_handler_suite(self, request):
|
||||
"""Retrieves two handlers (one for extra handshake processing, and one
|
||||
for data transfer) for the given request as a HandlerSuite object.
|
||||
"""
|
||||
|
||||
def _handler(self, request):
|
||||
try:
|
||||
ws_resource_path = request.ws_resource.split('?', 1)[0]
|
||||
return self._handlers[ws_resource_path]
|
||||
return self._handler_suite_map[ws_resource_path]
|
||||
except KeyError:
|
||||
raise DispatchError('No handler for: %r' % request.ws_resource)
|
||||
|
||||
def _source_files_in_dir(self, root_dir, scan_dir):
|
||||
def _source_handler_files_in_dir(self, root_dir, scan_dir):
|
||||
"""Source all the handler source files in the scan_dir directory.
|
||||
|
||||
The resource path is determined relative to root_dir.
|
||||
"""
|
||||
|
||||
to_resource = _path_to_resource_converter(root_dir)
|
||||
for path in _source_file_paths(scan_dir):
|
||||
convert = _create_path_to_resource_converter(root_dir)
|
||||
for path in _enumerate_handler_file_paths(scan_dir):
|
||||
try:
|
||||
handlers = _source(open(path).read())
|
||||
handler_suite = _source_handler_file(open(path).read())
|
||||
except DispatchError, e:
|
||||
self._source_warnings.append('%s: %s' % (path, e))
|
||||
continue
|
||||
self._handlers[to_resource(path)] = handlers
|
||||
self._handler_suite_map[convert(path)] = handler_suite
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
|
|
|
@ -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
|
||||
|
@ -28,30 +28,23 @@
|
|||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Web Socket handshaking.
|
||||
|
||||
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.
|
||||
"""WebSocket opening handshake processor. This class try to apply available
|
||||
opening handshake processors for each protocol version until a connection is
|
||||
successfully established.
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from mod_pywebsocket import util
|
||||
from mod_pywebsocket.handshake import draft75
|
||||
from mod_pywebsocket.handshake import handshake
|
||||
from mod_pywebsocket.handshake._base import DEFAULT_WEB_SOCKET_PORT
|
||||
from mod_pywebsocket.handshake._base import DEFAULT_WEB_SOCKET_SECURE_PORT
|
||||
from mod_pywebsocket.handshake._base import WEB_SOCKET_SCHEME
|
||||
from mod_pywebsocket.handshake._base import WEB_SOCKET_SECURE_SCHEME
|
||||
from mod_pywebsocket.handshake import hybi00
|
||||
from mod_pywebsocket.handshake import hybi06
|
||||
from mod_pywebsocket.handshake._base import HandshakeError
|
||||
from mod_pywebsocket.handshake._base import validate_protocol
|
||||
|
||||
|
||||
class Handshaker(object):
|
||||
"""This class performs Web Socket handshake."""
|
||||
"""This class performs WebSocket handshake."""
|
||||
|
||||
def __init__(self, request, dispatcher, allowDraft75=False, strict=False):
|
||||
"""Construct an instance.
|
||||
|
@ -68,28 +61,38 @@ class Handshaker(object):
|
|||
handshake.
|
||||
"""
|
||||
|
||||
self._logger = logging.getLogger("mod_pywebsocket.handshake")
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._request = request
|
||||
self._dispatcher = dispatcher
|
||||
self._strict = strict
|
||||
self._handshaker = handshake.Handshaker(request, dispatcher)
|
||||
self._fallbackHandshaker = None
|
||||
self._hybi07Handshaker = hybi06.Handshaker(request, dispatcher)
|
||||
self._hybi00Handshaker = hybi00.Handshaker(request, dispatcher)
|
||||
self._hixie75Handshaker = None
|
||||
if allowDraft75:
|
||||
self._fallbackHandshaker = draft75.Handshaker(
|
||||
self._hixie75Handshaker = draft75.Handshaker(
|
||||
request, dispatcher, strict)
|
||||
|
||||
def do_handshake(self):
|
||||
"""Perform Web Socket Handshake."""
|
||||
"""Perform WebSocket Handshake."""
|
||||
|
||||
try:
|
||||
self._handshaker.do_handshake()
|
||||
except HandshakeError, e:
|
||||
self._logger.error('Handshake error: %s' % e)
|
||||
if self._fallbackHandshaker:
|
||||
self._logger.warning('fallback to old protocol')
|
||||
self._fallbackHandshaker.do_handshake()
|
||||
return
|
||||
raise e
|
||||
self._logger.debug(
|
||||
'Opening handshake headers: %s' % self._request.headers_in)
|
||||
|
||||
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
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
|
|
|
@ -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
|
||||
|
@ -28,48 +28,51 @@
|
|||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Web Socket handshaking.
|
||||
|
||||
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.
|
||||
"""Common functions and exceptions used by WebSocket opening handshake
|
||||
processors.
|
||||
"""
|
||||
|
||||
|
||||
DEFAULT_WEB_SOCKET_PORT = 80
|
||||
DEFAULT_WEB_SOCKET_SECURE_PORT = 443
|
||||
WEB_SOCKET_SCHEME = 'ws'
|
||||
WEB_SOCKET_SECURE_SCHEME = 'wss'
|
||||
from mod_pywebsocket import common
|
||||
|
||||
|
||||
class HandshakeError(Exception):
|
||||
"""Exception in Web Socket Handshake."""
|
||||
|
||||
"""This exception will be raised when an error occurred while processing
|
||||
WebSocket initial handshake.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def default_port(is_secure):
|
||||
def get_default_port(is_secure):
|
||||
if is_secure:
|
||||
return DEFAULT_WEB_SOCKET_SECURE_PORT
|
||||
return common.DEFAULT_WEB_SOCKET_SECURE_PORT
|
||||
else:
|
||||
return DEFAULT_WEB_SOCKET_PORT
|
||||
return common.DEFAULT_WEB_SOCKET_PORT
|
||||
|
||||
|
||||
def validate_protocol(protocol):
|
||||
"""Validate WebSocket-Protocol string."""
|
||||
# TODO(tyoshino): Have stricter validator for HyBi 07.
|
||||
def validate_subprotocol(subprotocol):
|
||||
"""Validate a value in subprotocol fields such as WebSocket-Protocol,
|
||||
Sec-WebSocket-Protocol.
|
||||
|
||||
if not protocol:
|
||||
raise HandshakeError('Invalid WebSocket-Protocol: empty')
|
||||
for c in protocol:
|
||||
See
|
||||
- HyBi 06: Section 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 protocol: %r' % c)
|
||||
raise HandshakeError(
|
||||
'Illegal character in subprotocol name: %r' % c)
|
||||
|
||||
|
||||
def parse_host_header(request):
|
||||
fields = request.headers_in['Host'].split(':', 1)
|
||||
if len(fields) == 1:
|
||||
return fields[0], default_port(request.is_https())
|
||||
return fields[0], get_default_port(request.is_https())
|
||||
try:
|
||||
return fields[0], int(fields[1])
|
||||
except ValueError, e:
|
||||
|
@ -80,9 +83,9 @@ def build_location(request):
|
|||
"""Build WebSocket location for request."""
|
||||
location_parts = []
|
||||
if request.is_https():
|
||||
location_parts.append(WEB_SOCKET_SECURE_SCHEME)
|
||||
location_parts.append(common.WEB_SOCKET_SECURE_SCHEME)
|
||||
else:
|
||||
location_parts.append(WEB_SOCKET_SCHEME)
|
||||
location_parts.append(common.WEB_SOCKET_SCHEME)
|
||||
location_parts.append('://')
|
||||
host, port = parse_host_header(request)
|
||||
connection_port = request.connection.local_addr[1]
|
||||
|
@ -90,12 +93,34 @@ def build_location(request):
|
|||
raise HandshakeError('Header/connection port mismatch: %d/%d' %
|
||||
(port, connection_port))
|
||||
location_parts.append(host)
|
||||
if (port != default_port(request.is_https())):
|
||||
if (port != get_default_port(request.is_https())):
|
||||
location_parts.append(':')
|
||||
location_parts.append(str(port))
|
||||
location_parts.append(request.uri)
|
||||
return ''.join(location_parts)
|
||||
|
||||
|
||||
def get_mandatory_header(request, key, expected_value=None):
|
||||
value = request.headers_in.get(key)
|
||||
if value is None:
|
||||
raise HandshakeError('Header %s is not defined' % key)
|
||||
if expected_value is not None and expected_value != value:
|
||||
raise HandshakeError(
|
||||
'Illegal value for header %s: %s (expected: %s)' %
|
||||
(key, value, expected_value))
|
||||
return value
|
||||
|
||||
|
||||
def check_header_lines(request, mandatory_headers):
|
||||
# 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')
|
||||
# The expected field names, and the meaning of their corresponding
|
||||
# values, are as follows.
|
||||
# |Upgrade| and |Connection|
|
||||
for key, expected_value in mandatory_headers:
|
||||
get_mandatory_header(request, key, expected_value)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
|
|
|
@ -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
|
||||
|
@ -28,20 +28,24 @@
|
|||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Web Socket handshaking defined in draft-hixie-thewebsocketprotocol-75.
|
||||
|
||||
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.
|
||||
"""
|
||||
"""WebSocket handshaking defined in draft-hixie-thewebsocketprotocol-75."""
|
||||
|
||||
|
||||
# Note: request.connection.write is used in this module, even though mod_python
|
||||
# document says that it should be used only in connection handlers.
|
||||
# Unfortunately, we have no other options. For example, request.write is not
|
||||
# suitable because it doesn't allow direct raw bytes writing.
|
||||
|
||||
|
||||
import logging
|
||||
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 build_location
|
||||
from mod_pywebsocket.handshake._base import validate_protocol
|
||||
from mod_pywebsocket.handshake._base import validate_subprotocol
|
||||
|
||||
|
||||
_MANDATORY_HEADERS = [
|
||||
|
@ -70,7 +74,7 @@ _SIXTH_AND_LATER = re.compile(
|
|||
|
||||
|
||||
class Handshaker(object):
|
||||
"""This class performs Web Socket handshake."""
|
||||
"""This class performs WebSocket handshake."""
|
||||
|
||||
def __init__(self, request, dispatcher, strict=False):
|
||||
"""Construct an instance.
|
||||
|
@ -86,18 +90,28 @@ class Handshaker(object):
|
|||
handshake.
|
||||
"""
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._request = request
|
||||
self._dispatcher = dispatcher
|
||||
self._strict = strict
|
||||
|
||||
def do_handshake(self):
|
||||
"""Perform Web Socket Handshake."""
|
||||
"""Perform WebSocket Handshake.
|
||||
|
||||
On _request, we set
|
||||
ws_resource, ws_origin, ws_location, ws_protocol
|
||||
ws_challenge_md5: WebSocket handshake information.
|
||||
ws_stream: Frame generation/parsing class.
|
||||
ws_version: Protocol version.
|
||||
"""
|
||||
|
||||
self._check_header_lines()
|
||||
self._set_resource()
|
||||
self._set_origin()
|
||||
self._set_location()
|
||||
self._set_protocol()
|
||||
self._set_subprotocol()
|
||||
self._set_protocol_version()
|
||||
self._dispatcher.do_extra_handshake(self._request)
|
||||
self._send_handshake()
|
||||
|
||||
|
@ -110,28 +124,30 @@ class Handshaker(object):
|
|||
def _set_location(self):
|
||||
self._request.ws_location = build_location(self._request)
|
||||
|
||||
def _set_protocol(self):
|
||||
protocol = self._request.headers_in.get('WebSocket-Protocol')
|
||||
if protocol is not None:
|
||||
validate_protocol(protocol)
|
||||
self._request.ws_protocol = protocol
|
||||
def _set_subprotocol(self):
|
||||
subprotocol = self._request.headers_in.get('WebSocket-Protocol')
|
||||
if subprotocol is not None:
|
||||
validate_subprotocol(subprotocol)
|
||||
self._request.ws_protocol = subprotocol
|
||||
|
||||
def _set_protocol_version(self):
|
||||
self._logger.debug('IETF Hixie 75 protocol')
|
||||
self._request.ws_version = common.VERSION_HIXIE75
|
||||
self._request.ws_stream = StreamHixie75(self._request)
|
||||
|
||||
def _sendall(self, data):
|
||||
self._request.connection.write(data)
|
||||
|
||||
def _send_handshake(self):
|
||||
self._request.connection.write(
|
||||
'HTTP/1.1 101 Web Socket Protocol Handshake\r\n')
|
||||
self._request.connection.write('Upgrade: WebSocket\r\n')
|
||||
self._request.connection.write('Connection: Upgrade\r\n')
|
||||
self._request.connection.write('WebSocket-Origin: ')
|
||||
self._request.connection.write(self._request.ws_origin)
|
||||
self._request.connection.write('\r\n')
|
||||
self._request.connection.write('WebSocket-Location: ')
|
||||
self._request.connection.write(self._request.ws_location)
|
||||
self._request.connection.write('\r\n')
|
||||
self._sendall('HTTP/1.1 101 Web Socket Protocol Handshake\r\n')
|
||||
self._sendall('Upgrade: WebSocket\r\n')
|
||||
self._sendall('Connection: Upgrade\r\n')
|
||||
self._sendall('WebSocket-Origin: %s\r\n' % self._request.ws_origin)
|
||||
self._sendall('WebSocket-Location: %s\r\n' % self._request.ws_location)
|
||||
if self._request.ws_protocol:
|
||||
self._request.connection.write('WebSocket-Protocol: ')
|
||||
self._request.connection.write(self._request.ws_protocol)
|
||||
self._request.connection.write('\r\n')
|
||||
self._request.connection.write('\r\n')
|
||||
self._sendall(
|
||||
'WebSocket-Protocol: %s\r\n' % self._request.ws_protocol)
|
||||
self._sendall('\r\n')
|
||||
|
||||
def _check_header_lines(self):
|
||||
for key, expected_value in _MANDATORY_HEADERS:
|
||||
|
@ -140,8 +156,9 @@ class Handshaker(object):
|
|||
raise HandshakeError('Header %s is not defined' % key)
|
||||
if expected_value:
|
||||
if actual_value != expected_value:
|
||||
raise HandshakeError('Illegal value for header %s: %s' %
|
||||
(key, actual_value))
|
||||
raise HandshakeError(
|
||||
'Expected %r for header %s but found %r' %
|
||||
(expected_value, key, actual_value))
|
||||
if self._strict:
|
||||
try:
|
||||
lines = self._request.connection.get_memorized_lines()
|
||||
|
|
|
@ -1,208 +0,0 @@
|
|||
# Copyright 2009, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Web Socket handshaking.
|
||||
|
||||
Note: request.connection.write/read are used in this module, even though
|
||||
mod_python document says that they should be used only in connection handlers.
|
||||
Unfortunately, we have no other options. For example, request.write/read are
|
||||
not suitable because they don't allow direct raw bytes writing/reading.
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
from md5 import md5
|
||||
import re
|
||||
import struct
|
||||
|
||||
from mod_pywebsocket.handshake._base import HandshakeError
|
||||
from mod_pywebsocket.handshake._base import build_location
|
||||
from mod_pywebsocket.handshake._base import validate_protocol
|
||||
|
||||
|
||||
_MANDATORY_HEADERS = [
|
||||
# key, expected value or None
|
||||
['Upgrade', 'WebSocket'],
|
||||
['Connection', 'Upgrade'],
|
||||
]
|
||||
|
||||
def _hexify(s):
|
||||
return re.sub('.', lambda x: '%02x ' % ord(x.group(0)), s)
|
||||
|
||||
class Handshaker(object):
|
||||
"""This class performs Web Socket handshake."""
|
||||
|
||||
def __init__(self, request, dispatcher):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
dispatcher: Dispatcher (dispatch.Dispatcher).
|
||||
|
||||
Handshaker will add attributes such as ws_resource in performing
|
||||
handshake.
|
||||
"""
|
||||
|
||||
self._logger = logging.getLogger("mod_pywebsocket.handshake")
|
||||
self._request = request
|
||||
self._dispatcher = dispatcher
|
||||
|
||||
def do_handshake(self):
|
||||
"""Perform Web Socket Handshake."""
|
||||
|
||||
# 5.1 Reading the client's opening handshake.
|
||||
# dispatcher sets it in self._request.
|
||||
self._check_header_lines()
|
||||
self._set_resource()
|
||||
self._set_protocol()
|
||||
self._set_location()
|
||||
self._set_origin()
|
||||
self._set_challenge_response()
|
||||
self._dispatcher.do_extra_handshake(self._request)
|
||||
self._send_handshake()
|
||||
|
||||
def _check_header_lines(self):
|
||||
# 5.1 1. The three character UTF-8 string "GET".
|
||||
# 5.1 2. A UTF-8-encoded U+0020 SPACE character (0x20 byte).
|
||||
if self._request.method != 'GET':
|
||||
raise HandshakeError('Method is not GET')
|
||||
# The expected field names, and the meaning of their corresponding
|
||||
# values, are as follows.
|
||||
# |Upgrade| and |Connection|
|
||||
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)
|
||||
if expected_value:
|
||||
if actual_value != expected_value:
|
||||
raise HandshakeError('Illegal value for header %s: %s' %
|
||||
(key, actual_value))
|
||||
|
||||
def _set_resource(self):
|
||||
self._request.ws_resource = self._request.uri
|
||||
|
||||
def _set_protocol(self):
|
||||
# |Sec-WebSocket-Protocol|
|
||||
protocol = self._request.headers_in.get('Sec-WebSocket-Protocol')
|
||||
if protocol is not None:
|
||||
validate_protocol(protocol)
|
||||
self._request.ws_protocol = protocol
|
||||
|
||||
def _set_location(self):
|
||||
# |Host|
|
||||
host = self._request.headers_in.get('Host')
|
||||
if host is not None:
|
||||
self._request.ws_location = build_location(self._request)
|
||||
# TODO(ukai): check host is this host.
|
||||
|
||||
def _set_origin(self):
|
||||
# |Origin|
|
||||
origin = self._request.headers_in['Origin']
|
||||
if origin is not None:
|
||||
self._request.ws_origin = origin
|
||||
|
||||
def _set_challenge_response(self):
|
||||
# 5.2 4-8.
|
||||
self._request.ws_challenge = self._get_challenge()
|
||||
# 5.2 9. let /response/ be the MD5 finterprint of /challenge/
|
||||
self._request.ws_challenge_md5 = md5(
|
||||
self._request.ws_challenge).digest()
|
||||
self._logger.debug("challenge: %s" % _hexify(
|
||||
self._request.ws_challenge))
|
||||
self._logger.debug("response: %s" % _hexify(
|
||||
self._request.ws_challenge_md5))
|
||||
|
||||
def _get_key_value(self, key_field):
|
||||
key_value = self._request.headers_in.get(key_field)
|
||||
if key_value is None:
|
||||
self._logger.debug("no %s" % key_value)
|
||||
return None
|
||||
try:
|
||||
# 5.2 4. let /key-number_n/ be the digits (characters in the range
|
||||
# U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9)) in /key_n/,
|
||||
# interpreted as a base ten integer, ignoring all other characters
|
||||
# in /key_n/
|
||||
key_number = int(re.sub("\\D", "", key_value))
|
||||
# 5.2 5. let /spaces_n/ be the number of U+0020 SPACE characters
|
||||
# in /key_n/.
|
||||
spaces = re.subn(" ", "", key_value)[1]
|
||||
# 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('key_number %d is not an integral '
|
||||
'multiple of spaces %d' % (key_number,
|
||||
spaces))
|
||||
# 5.2 7. let /part_n/ be /key_number_n/ divided by /spaces_n/.
|
||||
part = key_number / spaces
|
||||
self._logger.debug("%s: %s => %d / %d => %d" % (
|
||||
key_field, key_value, key_number, spaces, part))
|
||||
return part
|
||||
except:
|
||||
return None
|
||||
|
||||
def _get_challenge(self):
|
||||
# 5.2 4-7.
|
||||
key1 = self._get_key_value('Sec-Websocket-Key1')
|
||||
if not key1:
|
||||
raise HandshakeError('Sec-WebSocket-Key1 not found')
|
||||
key2 = self._get_key_value('Sec-Websocket-Key2')
|
||||
if not key2:
|
||||
raise HandshakeError('Sec-WebSocket-Key2 not found')
|
||||
# 5.2 8. let /challenge/ be the concatenation of /part_1/,
|
||||
challenge = ""
|
||||
challenge += struct.pack("!I", key1) # network byteorder int
|
||||
challenge += struct.pack("!I", key2) # network byteorder int
|
||||
challenge += self._request.connection.read(8)
|
||||
return challenge
|
||||
|
||||
def _send_handshake(self):
|
||||
# 5.2 10. send the following line.
|
||||
self._request.connection.write(
|
||||
'HTTP/1.1 101 WebSocket Protocol Handshake\r\n')
|
||||
# 5.2 11. send the following fields to the client.
|
||||
self._request.connection.write('Upgrade: WebSocket\r\n')
|
||||
self._request.connection.write('Connection: Upgrade\r\n')
|
||||
self._request.connection.write('Sec-WebSocket-Location: ')
|
||||
self._request.connection.write(self._request.ws_location)
|
||||
self._request.connection.write('\r\n')
|
||||
self._request.connection.write('Sec-WebSocket-Origin: ')
|
||||
self._request.connection.write(self._request.ws_origin)
|
||||
self._request.connection.write('\r\n')
|
||||
if self._request.ws_protocol:
|
||||
self._request.connection.write('Sec-WebSocket-Protocol: ')
|
||||
self._request.connection.write(self._request.ws_protocol)
|
||||
self._request.connection.write('\r\n')
|
||||
# 5.2 12. send two bytes 0x0D 0x0A.
|
||||
self._request.connection.write('\r\n')
|
||||
# 5.2 13. send /response/
|
||||
self._request.connection.write(self._request.ws_challenge_md5)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -0,0 +1,232 @@
|
|||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""WebSocket initial handshake hander for HyBi 00 protocol."""
|
||||
|
||||
|
||||
# Note: request.connection.write/read are used in this module, even though
|
||||
# mod_python document says that they should be used only in connection
|
||||
# handlers. Unfortunately, we have no other options. For example,
|
||||
# request.write/read are not suitable because they don't allow direct raw bytes
|
||||
# writing/reading.
|
||||
|
||||
|
||||
import logging
|
||||
import re
|
||||
import struct
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket.stream import StreamHixie75
|
||||
from mod_pywebsocket import util
|
||||
from mod_pywebsocket.handshake._base import HandshakeError
|
||||
from mod_pywebsocket.handshake._base import build_location
|
||||
from mod_pywebsocket.handshake._base import check_header_lines
|
||||
from mod_pywebsocket.handshake._base import get_mandatory_header
|
||||
from mod_pywebsocket.handshake._base import validate_subprotocol
|
||||
|
||||
|
||||
_MANDATORY_HEADERS = [
|
||||
# key, expected value or None
|
||||
[common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75],
|
||||
[common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE],
|
||||
]
|
||||
|
||||
|
||||
class Handshaker(object):
|
||||
"""This class performs WebSocket handshake."""
|
||||
|
||||
def __init__(self, request, dispatcher):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
dispatcher: Dispatcher (dispatch.Dispatcher).
|
||||
|
||||
Handshaker will add attributes such as ws_resource in performing
|
||||
handshake.
|
||||
"""
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._request = request
|
||||
self._dispatcher = dispatcher
|
||||
|
||||
def do_handshake(self):
|
||||
"""Perform WebSocket Handshake.
|
||||
|
||||
On _request, we set
|
||||
ws_resource, ws_protocol, ws_location, ws_origin, ws_challenge,
|
||||
ws_challenge_md5: WebSocket handshake information.
|
||||
ws_stream: Frame generation/parsing class.
|
||||
ws_version: Protocol version.
|
||||
"""
|
||||
|
||||
# 5.1 Reading the client's opening handshake.
|
||||
# dispatcher sets it in self._request.
|
||||
check_header_lines(self._request, _MANDATORY_HEADERS)
|
||||
self._set_resource()
|
||||
self._set_subprotocol()
|
||||
self._set_location()
|
||||
self._set_origin()
|
||||
self._set_challenge_response()
|
||||
self._set_protocol_version()
|
||||
self._dispatcher.do_extra_handshake(self._request)
|
||||
self._send_handshake()
|
||||
|
||||
def _set_resource(self):
|
||||
self._request.ws_resource = self._request.uri
|
||||
|
||||
def _set_subprotocol(self):
|
||||
# |Sec-WebSocket-Protocol|
|
||||
subprotocol = self._request.headers_in.get(
|
||||
common.SEC_WEBSOCKET_PROTOCOL_HEADER)
|
||||
if subprotocol is not None:
|
||||
validate_subprotocol(subprotocol)
|
||||
self._request.ws_protocol = subprotocol
|
||||
|
||||
def _set_location(self):
|
||||
# |Host|
|
||||
host = self._request.headers_in.get(common.HOST_HEADER)
|
||||
if host is not None:
|
||||
self._request.ws_location = build_location(self._request)
|
||||
# TODO(ukai): check host is this host.
|
||||
|
||||
def _set_origin(self):
|
||||
# |Origin|
|
||||
origin = self._request.headers_in['Origin']
|
||||
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)
|
||||
|
||||
self._logger.debug('IETF HyBi 00 protocol')
|
||||
self._request.ws_version = common.VERSION_HYBI00
|
||||
self._request.ws_stream = StreamHixie75(self._request, True)
|
||||
|
||||
def _set_challenge_response(self):
|
||||
# 5.2 4-8.
|
||||
self._request.ws_challenge = self._get_challenge()
|
||||
# 5.2 9. let /response/ be the MD5 finterprint of /challenge/
|
||||
self._request.ws_challenge_md5 = util.md5_hash(
|
||||
self._request.ws_challenge).digest()
|
||||
self._logger.debug(
|
||||
'Challenge: %r (%s)' %
|
||||
(self._request.ws_challenge,
|
||||
util.hexify(self._request.ws_challenge)))
|
||||
self._logger.debug(
|
||||
'Challenge response: %r (%s)' %
|
||||
(self._request.ws_challenge_md5,
|
||||
util.hexify(self._request.ws_challenge_md5)))
|
||||
|
||||
def _get_key_value(self, key_field):
|
||||
key_value = get_mandatory_header(self._request, key_field)
|
||||
|
||||
# 5.2 4. let /key-number_n/ be the digits (characters in the range
|
||||
# U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9)) in /key_n/,
|
||||
# interpreted as a base ten integer, ignoring all other characters
|
||||
# in /key_n/.
|
||||
try:
|
||||
key_number = int(re.sub("\\D", "", key_value))
|
||||
except:
|
||||
raise HandshakeError('%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)
|
||||
# 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('Key-number %d is not an integral '
|
||||
'multiple of spaces %d' % (key_number,
|
||||
spaces))
|
||||
# 5.2 7. let /part_n/ be /key-number_n/ divided by /spaces_n/.
|
||||
part = key_number / spaces
|
||||
self._logger.debug('%s: %s => %d / %d => %d' % (
|
||||
key_field, key_value, key_number, spaces, part))
|
||||
return part
|
||||
|
||||
def _get_challenge(self):
|
||||
# 5.2 4-7.
|
||||
key1 = self._get_key_value('Sec-WebSocket-Key1')
|
||||
key2 = self._get_key_value('Sec-WebSocket-Key2')
|
||||
# 5.2 8. let /challenge/ be the concatenation of /part_1/,
|
||||
challenge = ''
|
||||
challenge += struct.pack('!I', key1) # network byteorder int
|
||||
challenge += struct.pack('!I', key2) # network byteorder int
|
||||
challenge += self._request.connection.read(8)
|
||||
return challenge
|
||||
|
||||
def _sendall(self, data):
|
||||
self._request.connection.write(data)
|
||||
|
||||
def _send_header(self, name, value):
|
||||
self._sendall('%s: %s\r\n' % (name, value))
|
||||
|
||||
def _send_handshake(self):
|
||||
# 5.2 10. send the following line.
|
||||
self._sendall('HTTP/1.1 101 WebSocket Protocol Handshake\r\n')
|
||||
# 5.2 11. send the following fields to the client.
|
||||
self._send_header(
|
||||
common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75)
|
||||
self._send_header(
|
||||
common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE)
|
||||
self._send_header('Sec-WebSocket-Location', self._request.ws_location)
|
||||
self._send_header(
|
||||
common.SEC_WEBSOCKET_ORIGIN_HEADER, self._request.ws_origin)
|
||||
if self._request.ws_protocol:
|
||||
self._send_header(
|
||||
common.SEC_WEBSOCKET_PROTOCOL_HEADER,
|
||||
self._request.ws_protocol)
|
||||
# 5.2 12. send two bytes 0x0D 0x0A.
|
||||
self._sendall('\r\n')
|
||||
# 5.2 13. send /response/
|
||||
self._sendall(self._request.ws_challenge_md5)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -0,0 +1,250 @@
|
|||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""WebSocket HyBi 07 opening handshake processor."""
|
||||
|
||||
|
||||
# Note: request.connection.write is used in this module, even though mod_python
|
||||
# document says that it should be used only in connection handlers.
|
||||
# Unfortunately, we have no other options. For example, request.write is not
|
||||
# suitable because it doesn't allow direct raw bytes writing.
|
||||
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket.stream import Stream
|
||||
from mod_pywebsocket.stream import StreamOptions
|
||||
from mod_pywebsocket import util
|
||||
from mod_pywebsocket.handshake._base import HandshakeError
|
||||
from mod_pywebsocket.handshake._base import check_header_lines
|
||||
from mod_pywebsocket.handshake._base import get_mandatory_header
|
||||
|
||||
|
||||
_MANDATORY_HEADERS = [
|
||||
# key, expected value or None
|
||||
[common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE],
|
||||
[common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE],
|
||||
]
|
||||
|
||||
_BASE64_REGEX = re.compile('^[+/0-9A-Za-z]*=*$')
|
||||
|
||||
|
||||
def compute_accept(key):
|
||||
"""Computes value for the Sec-WebSocket-Accept header from value of the
|
||||
Sec-WebSocket-Key header.
|
||||
"""
|
||||
|
||||
accept_binary = util.sha1_hash(
|
||||
key + common.WEBSOCKET_ACCEPT_UUID).digest()
|
||||
accept = base64.b64encode(accept_binary)
|
||||
|
||||
return (accept, accept_binary)
|
||||
|
||||
|
||||
class Handshaker(object):
|
||||
"""This class performs WebSocket handshake."""
|
||||
|
||||
def __init__(self, request, dispatcher):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
dispatcher: Dispatcher (dispatch.Dispatcher).
|
||||
|
||||
Handshaker will add attributes such as ws_resource during handshake.
|
||||
"""
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._request = request
|
||||
self._dispatcher = dispatcher
|
||||
|
||||
def do_handshake(self):
|
||||
check_header_lines(self._request, _MANDATORY_HEADERS)
|
||||
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)))
|
||||
|
||||
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.
|
||||
|
||||
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')
|
||||
|
||||
self._send_handshake(accept)
|
||||
|
||||
def _get_origin(self):
|
||||
origin = self._request.headers_in.get(
|
||||
common.SEC_WEBSOCKET_ORIGIN_HEADER)
|
||||
self._request.ws_origin = origin
|
||||
|
||||
def _check_version(self):
|
||||
unused_value = get_mandatory_header(
|
||||
self._request, common.SEC_WEBSOCKET_VERSION_HEADER, '7')
|
||||
|
||||
def _set_protocol(self):
|
||||
self._request.ws_protocol = None
|
||||
|
||||
protocol_header = self._request.headers_in.get(
|
||||
common.SEC_WEBSOCKET_PROTOCOL_HEADER)
|
||||
|
||||
if not protocol_header:
|
||||
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
|
||||
|
||||
extensions_header = self._request.headers_in.get(
|
||||
common.SEC_WEBSOCKET_EXTENSIONS_HEADER)
|
||||
if not extensions_header:
|
||||
self._request.ws_extensions = None
|
||||
return
|
||||
|
||||
self._request.ws_extensions = []
|
||||
|
||||
requested_extensions = extensions_header.split(',')
|
||||
# TODO(tyoshino): Follow the ABNF in the spec.
|
||||
requested_extensions = [s.strip() for s in requested_extensions]
|
||||
|
||||
for extension in requested_extensions:
|
||||
# We now support only deflate-stream extension. Any other
|
||||
# extension requests are just ignored for now.
|
||||
if extension == 'deflate-stream':
|
||||
self._request.ws_extensions.append(extension)
|
||||
self._request.ws_deflate = True
|
||||
|
||||
self._logger.debug('Extensions requested: %r', requested_extensions)
|
||||
self._logger.debug(
|
||||
'Extensions accepted: %r', self._request.ws_extensions)
|
||||
|
||||
def _validate_key(self, key):
|
||||
# Validate
|
||||
key_is_valid = False
|
||||
try:
|
||||
# Validate key by quick regex match before parsing by base64
|
||||
# module. Because base64 module skips invalid characters, we have
|
||||
# to do this in advance to make this server strictly reject illegal
|
||||
# keys.
|
||||
if _BASE64_REGEX.match(key):
|
||||
decoded_key = base64.b64decode(key)
|
||||
if len(decoded_key) == 16:
|
||||
key_is_valid = True
|
||||
except TypeError, e:
|
||||
pass
|
||||
|
||||
if not key_is_valid:
|
||||
raise HandshakeError(
|
||||
'Illegal value for header %s: %r' %
|
||||
(common.SEC_WEBSOCKET_KEY_HEADER, key))
|
||||
|
||||
return decoded_key
|
||||
|
||||
def _get_key(self):
|
||||
key = get_mandatory_header(
|
||||
self._request, common.SEC_WEBSOCKET_KEY_HEADER)
|
||||
|
||||
decoded_key = self._validate_key(key)
|
||||
|
||||
self._logger.debug('Sec-WebSocket-Key: %r (%s)' %
|
||||
(key, util.hexify(decoded_key)))
|
||||
|
||||
return key
|
||||
|
||||
def _sendall(self, data):
|
||||
self._request.connection.write(data)
|
||||
|
||||
def _send_header(self, name, value):
|
||||
self._sendall('%s: %s\r\n' % (name, value))
|
||||
|
||||
def _send_handshake(self, accept):
|
||||
self._sendall('HTTP/1.1 101 Switching Protocols\r\n')
|
||||
self._send_header(common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE)
|
||||
self._send_header(
|
||||
common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE)
|
||||
self._send_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:
|
||||
self._send_header(
|
||||
common.SEC_WEBSOCKET_PROTOCOL_HEADER,
|
||||
self._request.ws_protocol)
|
||||
if self._request.ws_extensions is not None:
|
||||
self._send_header(
|
||||
common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
|
||||
', '.join(self._request.ws_extensions))
|
||||
self._sendall('\r\n')
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -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
|
||||
|
@ -31,9 +31,10 @@
|
|||
"""PythonHeaderParserHandler for mod_pywebsocket.
|
||||
|
||||
Apache HTTP Server and mod_python must be configured such that this
|
||||
function is called to handle Web Socket request.
|
||||
function is called to handle WebSocket request.
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
from mod_python import apache
|
||||
|
@ -57,7 +58,8 @@ _PYOPT_ALLOW_DRAFT75 = 'mod_pywebsocket.allow_draft75'
|
|||
|
||||
|
||||
class ApacheLogHandler(logging.Handler):
|
||||
"""Wrapper logging.Handler to emit log message to apache's error.log"""
|
||||
"""Wrapper logging.Handler to emit log message to apache's error.log."""
|
||||
|
||||
_LEVELS = {
|
||||
logging.DEBUG: apache.APLOG_DEBUG,
|
||||
logging.INFO: apache.APLOG_INFO,
|
||||
|
@ -65,11 +67,12 @@ class ApacheLogHandler(logging.Handler):
|
|||
logging.ERROR: apache.APLOG_ERR,
|
||||
logging.CRITICAL: apache.APLOG_CRIT,
|
||||
}
|
||||
|
||||
def __init__(self, request=None):
|
||||
logging.Handler.__init__(self)
|
||||
self.log_error = apache.log_error
|
||||
if request is not None:
|
||||
self.log_error = request.log_error
|
||||
self.log_error = request.log_error
|
||||
|
||||
def emit(self, record):
|
||||
apache_level = apache.APLOG_DEBUG
|
||||
|
@ -78,7 +81,7 @@ class ApacheLogHandler(logging.Handler):
|
|||
self.log_error(record.getMessage(), apache_level)
|
||||
|
||||
|
||||
logging.getLogger("mod_pywebsocket").addHandler(ApacheLogHandler())
|
||||
logging.getLogger('mod_pywebsocket').addHandler(ApacheLogHandler())
|
||||
|
||||
|
||||
def _create_dispatcher():
|
||||
|
@ -111,12 +114,13 @@ def headerparserhandler(request):
|
|||
|
||||
try:
|
||||
allowDraft75 = apache.main_server.get_options().get(
|
||||
_PYOPT_ALLOW_DRAFT75, None)
|
||||
_PYOPT_ALLOW_DRAFT75, None)
|
||||
handshaker = handshake.Handshaker(request, _dispatcher,
|
||||
allowDraft75=allowDraft75)
|
||||
handshaker.do_handshake()
|
||||
request.log_error('mod_pywebsocket: resource: %r' % request.ws_resource,
|
||||
apache.APLOG_DEBUG)
|
||||
request.log_error(
|
||||
'mod_pywebsocket: resource: %r' % request.ws_resource,
|
||||
apache.APLOG_DEBUG)
|
||||
try:
|
||||
_dispatcher.transfer_data(request)
|
||||
except Exception, e:
|
||||
|
@ -132,6 +136,8 @@ def headerparserhandler(request):
|
|||
except dispatch.DispatchError, e:
|
||||
request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_WARNING)
|
||||
return apache.DECLINED
|
||||
# Set assbackwards to suppress response header generation by Apache.
|
||||
request.assbackwards = 1
|
||||
return apache.DONE # Return DONE such that no other handlers are invoked.
|
||||
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ class MemorizingFile(object):
|
|||
file_: the file object to wrap.
|
||||
max_memorized_lines: the maximum number of lines to memorize.
|
||||
Only the first max_memorized_lines are memorized.
|
||||
Default: sys.maxint.
|
||||
Default: sys.maxint.
|
||||
"""
|
||||
self._file = file_
|
||||
self._memorized_lines = []
|
||||
|
|
|
@ -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
|
||||
|
@ -40,153 +40,52 @@ not suitable because they don't allow direct raw bytes writing/reading.
|
|||
import Queue
|
||||
import threading
|
||||
|
||||
from mod_pywebsocket import util
|
||||
from time import time,sleep
|
||||
|
||||
# Export Exception symbols from msgutil for backward compatibility
|
||||
from mod_pywebsocket._stream_base import ConnectionTerminatedException
|
||||
from mod_pywebsocket._stream_base import InvalidFrameException
|
||||
from mod_pywebsocket._stream_base import BadOperationException
|
||||
from mod_pywebsocket._stream_base import UnsupportedFrameException
|
||||
|
||||
|
||||
class MsgUtilException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionTerminatedException(MsgUtilException):
|
||||
pass
|
||||
|
||||
|
||||
def _read(request, length):
|
||||
bytes = request.connection.read(length)
|
||||
if not bytes:
|
||||
raise MsgUtilException(
|
||||
'Failed to receive message from %r' %
|
||||
(request.connection.remote_addr,))
|
||||
return bytes
|
||||
|
||||
|
||||
def _write(request, bytes):
|
||||
try:
|
||||
request.connection.write(bytes)
|
||||
except Exception, e:
|
||||
util.prepend_message_to_exception(
|
||||
'Failed to send message to %r: ' %
|
||||
(request.connection.remote_addr,),
|
||||
e)
|
||||
raise
|
||||
|
||||
|
||||
def close_connection(request, abort=False):
|
||||
# An API for handler to send/receive WebSocket messages.
|
||||
def close_connection(request):
|
||||
"""Close connection.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
"""
|
||||
if request.server_terminated:
|
||||
return
|
||||
# 5.3 the server may decide to terminate the WebSocket connection by
|
||||
# running through the following steps:
|
||||
# 1. send a 0xFF byte and a 0x00 byte to the client to indicate the start
|
||||
# of the closing handshake.
|
||||
got_exception = False
|
||||
if not abort:
|
||||
_write(request, '\xff\x00')
|
||||
# timeout of 20 seconds to get the client's close frame ack
|
||||
initial_time = time()
|
||||
end_time = initial_time + 20
|
||||
while time() < end_time:
|
||||
try:
|
||||
receive_message(request)
|
||||
except ConnectionTerminatedException, e:
|
||||
got_exception = True
|
||||
sleep(1)
|
||||
request.server_terminated = True
|
||||
if got_exception:
|
||||
util.prepend_message_to_exception(
|
||||
'client initiated closing handshake for %s: ' % (
|
||||
request.ws_resource),
|
||||
e)
|
||||
raise ConnectionTerminatedException
|
||||
# TODO: 3. close the WebSocket connection.
|
||||
# note: mod_python Connection (mp_conn) doesn't have close method.
|
||||
request.ws_stream.close_connection()
|
||||
|
||||
|
||||
def send_message(request, message):
|
||||
def send_message(request, message, end=True):
|
||||
"""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.
|
||||
Raises:
|
||||
ConnectionTerminatedException: when server already terminated.
|
||||
BadOperationException: when server already terminated.
|
||||
"""
|
||||
if request.server_terminated:
|
||||
raise ConnectionTerminatedException
|
||||
_write(request, '\x00' + message.encode('utf-8') + '\xff')
|
||||
request.ws_stream.send_message(message, end)
|
||||
|
||||
|
||||
def receive_message(request):
|
||||
"""Receive a Web Socket frame and return its payload as unicode string.
|
||||
"""Receive a WebSocket frame and return its payload as unicode string.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
Raises:
|
||||
ConnectionTerminatedException: when client already terminated.
|
||||
BadOperationException: when client already terminated.
|
||||
"""
|
||||
|
||||
if request.client_terminated:
|
||||
raise ConnectionTerminatedException
|
||||
while True:
|
||||
# Read 1 byte.
|
||||
# mp_conn.read will block if no bytes are available.
|
||||
# Timeout is controlled by TimeOut directive of Apache.
|
||||
frame_type_str = _read(request, 1)
|
||||
frame_type = ord(frame_type_str[0])
|
||||
if (frame_type & 0x80) == 0x80:
|
||||
# The payload length is specified in the frame.
|
||||
# Read and discard.
|
||||
length = _payload_length(request)
|
||||
_receive_bytes(request, length)
|
||||
# 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the
|
||||
# /client terminated/ flag and abort these steps.
|
||||
if frame_type == 0xFF and length == 0:
|
||||
request.client_terminated = True
|
||||
raise ConnectionTerminatedException
|
||||
else:
|
||||
# The payload is delimited with \xff.
|
||||
bytes = _read_until(request, '\xff')
|
||||
# The Web Socket protocol section 4.4 specifies that invalid
|
||||
# characters must be replaced with U+fffd REPLACEMENT CHARACTER.
|
||||
message = bytes.decode('utf-8', 'replace')
|
||||
if frame_type == 0x00:
|
||||
return message
|
||||
# Discard data of other types.
|
||||
return request.ws_stream.receive_message()
|
||||
|
||||
|
||||
def _payload_length(request):
|
||||
length = 0
|
||||
while True:
|
||||
b_str = _read(request, 1)
|
||||
b = ord(b_str[0])
|
||||
length = length * 128 + (b & 0x7f)
|
||||
if (b & 0x80) == 0:
|
||||
break
|
||||
return length
|
||||
|
||||
|
||||
def _receive_bytes(request, length):
|
||||
bytes = []
|
||||
while length > 0:
|
||||
new_bytes = _read(request, length)
|
||||
bytes.append(new_bytes)
|
||||
length -= len(new_bytes)
|
||||
return ''.join(bytes)
|
||||
|
||||
|
||||
def _read_until(request, delim_char):
|
||||
bytes = []
|
||||
while True:
|
||||
ch = _read(request, 1)
|
||||
if ch == delim_char:
|
||||
break
|
||||
bytes.append(ch)
|
||||
return ''.join(bytes)
|
||||
def send_ping(request, body=''):
|
||||
request.ws_stream.send_ping(body)
|
||||
|
||||
|
||||
class MessageReceiver(threading.Thread):
|
||||
|
@ -199,6 +98,7 @@ class MessageReceiver(threading.Thread):
|
|||
because pyOpenSSL used by the server raises a fatal error if the socket
|
||||
is accessed from multiple threads.
|
||||
"""
|
||||
|
||||
def __init__(self, request, onmessage=None):
|
||||
"""Construct an instance.
|
||||
|
||||
|
@ -210,6 +110,7 @@ class MessageReceiver(threading.Thread):
|
|||
and MessageReceiver.receive_nowait are useless because
|
||||
they will never return any messages.
|
||||
"""
|
||||
|
||||
threading.Thread.__init__(self)
|
||||
self._request = request
|
||||
self._queue = Queue.Queue()
|
||||
|
@ -269,6 +170,7 @@ class MessageSender(threading.Thread):
|
|||
because pyOpenSSL used by the server raises a fatal error if the socket
|
||||
is accessed from multiple threads.
|
||||
"""
|
||||
|
||||
def __init__(self, request):
|
||||
"""Construct an instance.
|
||||
|
||||
|
|
|
@ -0,0 +1,476 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Standalone WebSocket server.
|
||||
|
||||
Use this server to run mod_pywebsocket without Apache HTTP Server.
|
||||
|
||||
Usage:
|
||||
python standalone.py [-p <ws_port>] [-w <websock_handlers>]
|
||||
[-s <scan_dir>]
|
||||
[-d <document_root>]
|
||||
[-m <websock_handlers_map_file>]
|
||||
... for other options, see _main below ...
|
||||
|
||||
<ws_port> is the port number to use for ws:// connection.
|
||||
|
||||
<document_root> is the path to the root directory of HTML files.
|
||||
|
||||
<websock_handlers> is the path to the root directory of WebSocket handlers.
|
||||
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.
|
||||
|
||||
Note:
|
||||
This server is derived from SocketServer.ThreadingMixIn. Hence a thread is
|
||||
used for each request.
|
||||
|
||||
SECURITY WARNING: This uses CGIHTTPServer and CGIHTTPServer is not secure.
|
||||
It may execute arbitrary Python code or external programs. It should not be
|
||||
used outside a firewall.
|
||||
"""
|
||||
|
||||
import BaseHTTPServer
|
||||
import CGIHTTPServer
|
||||
import SimpleHTTPServer
|
||||
import SocketServer
|
||||
import logging
|
||||
import logging.handlers
|
||||
import optparse
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
|
||||
_HAS_OPEN_SSL = False
|
||||
try:
|
||||
import OpenSSL.SSL
|
||||
_HAS_OPEN_SSL = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket import dispatch
|
||||
from mod_pywebsocket import handshake
|
||||
from mod_pywebsocket import memorizingfile
|
||||
from mod_pywebsocket import util
|
||||
|
||||
|
||||
_DEFAULT_LOG_MAX_BYTES = 1024 * 256
|
||||
_DEFAULT_LOG_BACKUP_COUNT = 5
|
||||
|
||||
_DEFAULT_REQUEST_QUEUE_SIZE = 128
|
||||
|
||||
# 1024 is practically large enough to contain WebSocket handshake lines.
|
||||
_MAX_MEMORIZED_LINES = 1024
|
||||
|
||||
|
||||
def _print_warnings_if_any(dispatcher):
|
||||
warnings = dispatcher.source_warnings()
|
||||
if warnings:
|
||||
for warning in warnings:
|
||||
logging.warning('mod_pywebsocket: %s' % warning)
|
||||
|
||||
|
||||
class _StandaloneConnection(object):
|
||||
"""Mimic mod_python mp_conn."""
|
||||
|
||||
def __init__(self, request_handler):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request_handler: A WebSocketRequestHandler instance.
|
||||
"""
|
||||
self._request_handler = request_handler
|
||||
|
||||
def get_local_addr(self):
|
||||
"""Getter to mimic mp_conn.local_addr."""
|
||||
return (self._request_handler.server.server_name,
|
||||
self._request_handler.server.server_port)
|
||||
local_addr = property(get_local_addr)
|
||||
|
||||
def get_remote_addr(self):
|
||||
"""Getter to mimic mp_conn.remote_addr.
|
||||
|
||||
Setting the property in __init__ won't work because the request
|
||||
handler is not initialized yet there."""
|
||||
return self._request_handler.client_address
|
||||
remote_addr = property(get_remote_addr)
|
||||
|
||||
def write(self, data):
|
||||
"""Mimic mp_conn.write()."""
|
||||
return self._request_handler.wfile.write(data)
|
||||
|
||||
def read(self, length):
|
||||
"""Mimic mp_conn.read()."""
|
||||
return self._request_handler.rfile.read(length)
|
||||
|
||||
def get_memorized_lines(self):
|
||||
"""Get memorized lines."""
|
||||
return self._request_handler.rfile.get_memorized_lines()
|
||||
|
||||
|
||||
class _StandaloneRequest(object):
|
||||
"""Mimic mod_python request."""
|
||||
|
||||
def __init__(self, request_handler, use_tls):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request_handler: A WebSocketRequestHandler instance.
|
||||
"""
|
||||
self._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
|
||||
|
||||
|
||||
class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
||||
"""HTTPServer specialized for WebSocket."""
|
||||
|
||||
SocketServer.ThreadingMixIn.daemon_threads = True
|
||||
SocketServer.TCPServer.allow_reuse_address = True
|
||||
|
||||
def __init__(self, server_address, RequestHandlerClass):
|
||||
"""Override SocketServer.BaseServer.__init__."""
|
||||
|
||||
SocketServer.BaseServer.__init__(
|
||||
self, server_address, RequestHandlerClass)
|
||||
self.socket = self._create_socket()
|
||||
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 handle_error(self, rquest, client_address):
|
||||
"""Override SocketServer.handle_error."""
|
||||
|
||||
logging.error(
|
||||
('Exception in processing request from: %r' % (client_address,)) +
|
||||
'\n' + util.get_stack_trace())
|
||||
# Note: client_address is a tuple. To match it against %r, we need the
|
||||
# trailing comma.
|
||||
|
||||
|
||||
class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
|
||||
"""CGIHTTPRequestHandler specialized for WebSocket."""
|
||||
|
||||
def setup(self):
|
||||
"""Override SocketServer.StreamRequestHandler.setup to wrap rfile with
|
||||
MemorizingFile.
|
||||
"""
|
||||
|
||||
# Call superclass's setup to prepare rfile, wfile, etc. See setup
|
||||
# definition on the root class SocketServer.StreamRequestHandler to
|
||||
# understand what this does.
|
||||
CGIHTTPServer.CGIHTTPRequestHandler.setup(self)
|
||||
|
||||
self.rfile = memorizingfile.MemorizingFile(
|
||||
self.rfile,
|
||||
max_memorized_lines=_MAX_MEMORIZED_LINES)
|
||||
|
||||
def __init__(self, *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 _print_warnings_if_any(self):
|
||||
warnings = self._dispatcher.source_warnings()
|
||||
if warnings:
|
||||
for warning in warnings:
|
||||
logging.warning('mod_pywebsocket: %s' % warning)
|
||||
|
||||
def parse_request(self):
|
||||
"""Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request.
|
||||
|
||||
Return True to continue processing for HTTP(S), False otherwise.
|
||||
"""
|
||||
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)
|
||||
return True
|
||||
except dispatch.DispatchError, e:
|
||||
logging.warning('mod_pywebsocket: %s' % e)
|
||||
return False
|
||||
except Exception, e:
|
||||
logging.warning('mod_pywebsocket: %s' % e)
|
||||
logging.warning('mod_pywebsocket: %s' % util.get_stack_trace())
|
||||
return False
|
||||
return result
|
||||
|
||||
def log_request(self, code='-', size='-'):
|
||||
"""Override BaseHTTPServer.log_request."""
|
||||
|
||||
logging.info('"%s" %s %s',
|
||||
self.requestline, str(code), str(size))
|
||||
|
||||
def log_error(self, *args):
|
||||
"""Override BaseHTTPServer.log_error."""
|
||||
|
||||
# Despite the name, this method is for warnings than for errors.
|
||||
# For example, HTTP status code is logged by this method.
|
||||
logging.warn('%s - %s' % (self.address_string(), (args[0] % args[1:])))
|
||||
|
||||
def is_cgi(self):
|
||||
"""Test whether self.path corresponds to a CGI script.
|
||||
|
||||
Add extra check that self.path doesn't contains ..
|
||||
Also check if the file is a executable file or not.
|
||||
If the file is not executable, it is handled as static file or dir
|
||||
rather than a CGI script.
|
||||
"""
|
||||
if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self):
|
||||
if '..' in self.path:
|
||||
return False
|
||||
# strip query parameter from request path
|
||||
resource_name = self.path.split('?', 2)[0]
|
||||
# convert resource_name into real path name in filesystem.
|
||||
scriptfile = self.translate_path(resource_name)
|
||||
if not os.path.isfile(scriptfile):
|
||||
return False
|
||||
if not self.is_executable(scriptfile):
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _configure_logging(options):
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.getLevelName(options.log_level.upper()))
|
||||
if options.log_file:
|
||||
handler = logging.handlers.RotatingFileHandler(
|
||||
options.log_file, 'a', options.log_max, options.log_count)
|
||||
else:
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter(
|
||||
'[%(asctime)s] [%(levelname)s] %(name)s: %(message)s')
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
|
||||
def _alias_handlers(dispatcher, websock_handlers_map_file):
|
||||
"""Set aliases specified in websock_handler_map_file in dispatcher.
|
||||
|
||||
Args:
|
||||
dispatcher: dispatch.Dispatcher instance
|
||||
websock_handler_map_file: alias map file
|
||||
"""
|
||||
fp = open(websock_handlers_map_file)
|
||||
try:
|
||||
for line in fp:
|
||||
if line[0] == '#' or line.isspace():
|
||||
continue
|
||||
m = re.match('(\S+)\s+(\S+)', line)
|
||||
if not m:
|
||||
logging.warning('Wrong format in map file:' + line)
|
||||
continue
|
||||
try:
|
||||
dispatcher.add_resource_path_alias(
|
||||
m.group(1), m.group(2))
|
||||
except dispatch.DispatchError, e:
|
||||
logging.error(str(e))
|
||||
finally:
|
||||
fp.close()
|
||||
|
||||
|
||||
def _main():
|
||||
parser = optparse.OptionParser()
|
||||
parser.add_option('-H', '--server-host', '--server_host',
|
||||
dest='server_host',
|
||||
default='',
|
||||
help='server hostname to listen to')
|
||||
parser.add_option('-p', '--port', dest='port', type='int',
|
||||
default=common.DEFAULT_WEB_SOCKET_PORT,
|
||||
help='port to listen to')
|
||||
parser.add_option('-w', '--websock-handlers', '--websock_handlers',
|
||||
dest='websock_handlers',
|
||||
default='.',
|
||||
help='WebSocket handlers root directory.')
|
||||
parser.add_option('-m', '--websock-handlers-map-file',
|
||||
'--websock_handlers_map_file',
|
||||
dest='websock_handlers_map_file',
|
||||
default=None,
|
||||
help=('WebSocket handlers map file. '
|
||||
'Each line consists of alias_resource_path and '
|
||||
'existing_resource_path, separated by spaces.'))
|
||||
parser.add_option('-s', '--scan-dir', '--scan_dir', dest='scan_dir',
|
||||
default=None,
|
||||
help=('WebSocket handlers scan directory. '
|
||||
'Must be a directory under websock_handlers.'))
|
||||
parser.add_option('-d', '--document-root', '--document_root',
|
||||
dest='document_root', default='.',
|
||||
help='Document root directory.')
|
||||
parser.add_option('-x', '--cgi-paths', '--cgi_paths', dest='cgi_paths',
|
||||
default=None,
|
||||
help=('CGI paths relative to document_root.'
|
||||
'Comma-separated. (e.g -x /cgi,/htbin) '
|
||||
'Files under document_root/cgi_path are handled '
|
||||
'as CGI programs. Must be executable.'))
|
||||
parser.add_option('-t', '--tls', dest='use_tls', action='store_true',
|
||||
default=False, help='use TLS (wss://)')
|
||||
parser.add_option('-k', '--private-key', '--private_key',
|
||||
dest='private_key',
|
||||
default='', help='TLS private key file.')
|
||||
parser.add_option('-c', '--certificate', dest='certificate',
|
||||
default='', help='TLS certificate file.')
|
||||
parser.add_option('-l', '--log-file', '--log_file', dest='log_file',
|
||||
default='', help='Log file.')
|
||||
parser.add_option('--log-level', '--log_level', type='choice',
|
||||
dest='log_level', default='warn',
|
||||
choices=['debug', 'info', 'warning', 'warn', 'error',
|
||||
'critical'],
|
||||
help='Log level.')
|
||||
parser.add_option('--log-max', '--log_max', dest='log_max', type='int',
|
||||
default=_DEFAULT_LOG_MAX_BYTES,
|
||||
help='Log maximum bytes')
|
||||
parser.add_option('--log-count', '--log_count', dest='log_count',
|
||||
type='int', default=_DEFAULT_LOG_BACKUP_COUNT,
|
||||
help='Log backup count')
|
||||
parser.add_option('--allow-draft75', dest='allow_draft75',
|
||||
action='store_true', default=False,
|
||||
help='Allow draft 75 handshake')
|
||||
parser.add_option('--strict', dest='strict', action='store_true',
|
||||
default=False, help='Strictly check handshake request')
|
||||
parser.add_option('-q', '--queue', dest='request_queue_size', type='int',
|
||||
default=_DEFAULT_REQUEST_QUEUE_SIZE,
|
||||
help='request queue size')
|
||||
options = parser.parse_args()[0]
|
||||
|
||||
os.chdir(options.document_root)
|
||||
|
||||
_configure_logging(options)
|
||||
|
||||
SocketServer.TCPServer.request_queue_size = options.request_queue_size
|
||||
CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = []
|
||||
|
||||
if options.cgi_paths:
|
||||
CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = \
|
||||
options.cgi_paths.split(',')
|
||||
if sys.platform in ('cygwin', 'win32'):
|
||||
cygwin_path = None
|
||||
# For Win32 Python, it is expected that CYGWIN_PATH
|
||||
# is set to a directory of cygwin binaries.
|
||||
# For example, websocket_server.py in Chromium sets CYGWIN_PATH to
|
||||
# full path of third_party/cygwin/bin.
|
||||
if 'CYGWIN_PATH' in os.environ:
|
||||
cygwin_path = os.environ['CYGWIN_PATH']
|
||||
util.wrap_popen3_for_win(cygwin_path)
|
||||
def __check_script(scriptpath):
|
||||
return util.get_script_interp(scriptpath, cygwin_path)
|
||||
CGIHTTPServer.executable = __check_script
|
||||
|
||||
if options.use_tls:
|
||||
if not _HAS_OPEN_SSL:
|
||||
logging.critical('To use TLS, install pyOpenSSL.')
|
||||
sys.exit(1)
|
||||
if not options.private_key or not options.certificate:
|
||||
logging.critical(
|
||||
'To use TLS, specify private_key and certificate.')
|
||||
sys.exit(1)
|
||||
|
||||
if not options.scan_dir:
|
||||
options.scan_dir = options.websock_handlers
|
||||
|
||||
try:
|
||||
# Share a Dispatcher among request handlers to save time for
|
||||
# instantiation. Dispatcher can be shared because it is thread-safe.
|
||||
options.dispatcher = dispatch.Dispatcher(options.websock_handlers,
|
||||
options.scan_dir)
|
||||
if options.websock_handlers_map_file:
|
||||
_alias_handlers(options.dispatcher,
|
||||
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.serve_forever()
|
||||
except Exception, e:
|
||||
logging.critical('mod_pywebsocket: %s' % e)
|
||||
logging.critical('mod_pywebsocket: %s' % util.get_stack_trace())
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_main()
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -0,0 +1,53 @@
|
|||
# Copyright 2010, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""This file exports public symbols.
|
||||
"""
|
||||
|
||||
|
||||
from mod_pywebsocket._stream_base import BadOperationException
|
||||
from mod_pywebsocket._stream_base import ConnectionTerminatedException
|
||||
from mod_pywebsocket._stream_base import InvalidFrameException
|
||||
from mod_pywebsocket._stream_base import UnsupportedFrameException
|
||||
from mod_pywebsocket._stream_hixie75 import StreamHixie75
|
||||
from mod_pywebsocket._stream_hybi06 import Stream
|
||||
from mod_pywebsocket._stream_hybi06 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
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -28,14 +28,31 @@
|
|||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Web Sockets utilities.
|
||||
"""WebSocket utilities.
|
||||
"""
|
||||
|
||||
|
||||
import array
|
||||
|
||||
# Import hash classes from a module available and recommended for each Python
|
||||
# version and re-export those symbol. Use sha and md5 module in Python 2.4, and
|
||||
# hashlib module in Python 2.6.
|
||||
try:
|
||||
import hashlib
|
||||
md5_hash = hashlib.md5
|
||||
sha1_hash = hashlib.sha1
|
||||
except ImportError:
|
||||
import md5
|
||||
import sha
|
||||
md5_hash = md5.md5
|
||||
sha1_hash = sha.sha
|
||||
|
||||
import StringIO
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
import zlib
|
||||
|
||||
|
||||
def get_stack_trace():
|
||||
|
@ -72,7 +89,7 @@ def __translate_interp(interp, cygwin_path):
|
|||
"""
|
||||
if not cygwin_path:
|
||||
return interp
|
||||
m = re.match("^[^ ]*/([^ ]+)( .*)?", interp)
|
||||
m = re.match('^[^ ]*/([^ ]+)( .*)?', interp)
|
||||
if m:
|
||||
cmd = os.path.join(cygwin_path, m.group(1))
|
||||
return cmd + m.group(2)
|
||||
|
@ -96,7 +113,7 @@ def get_script_interp(script_path, cygwin_path=None):
|
|||
fp = open(script_path)
|
||||
line = fp.readline()
|
||||
fp.close()
|
||||
m = re.match("^#!(.*)", line)
|
||||
m = re.match('^#!(.*)', line)
|
||||
if m:
|
||||
return __translate_interp(m.group(1), cygwin_path)
|
||||
return None
|
||||
|
@ -113,9 +130,200 @@ def wrap_popen3_for_win(cygwin_path):
|
|||
cmdline = cmd.split(' ')
|
||||
interp = get_script_interp(cmdline[0], cygwin_path)
|
||||
if interp:
|
||||
cmd = interp + " " + cmd
|
||||
cmd = interp + ' ' + cmd
|
||||
return __orig_popen3(cmd, mode, bufsize)
|
||||
os.popen3 = __wrap_popen3
|
||||
|
||||
|
||||
def hexify(s):
|
||||
return ' '.join(map(lambda x: '%02x' % ord(x), s))
|
||||
|
||||
|
||||
def get_class_logger(o):
|
||||
return logging.getLogger(
|
||||
'%s.%s' % (o.__class__.__module__, o.__class__.__name__))
|
||||
|
||||
|
||||
class NoopMasker(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def mask(self, s):
|
||||
return s
|
||||
|
||||
|
||||
class RepeatedXorMasker(object):
|
||||
"""A masking object that applies XOR on the string given to mask method
|
||||
with the masking bytes given to the constructor repeatedly. This object
|
||||
remembers the position in the masking bytes the last mask method call ended
|
||||
and resumes from that point on the next mask method call.
|
||||
"""
|
||||
|
||||
def __init__(self, mask):
|
||||
self._mask = map(ord, mask)
|
||||
self._mask_size = len(self._mask)
|
||||
self._count = 0
|
||||
|
||||
def mask(self, s):
|
||||
result = array.array('B')
|
||||
result.fromstring(s)
|
||||
for i in xrange(len(result)):
|
||||
result[i] ^= self._mask[self._count]
|
||||
self._count = (self._count + 1) % self._mask_size
|
||||
return result.tostring()
|
||||
|
||||
|
||||
class DeflateRequest(object):
|
||||
"""A wrapper class for request object to intercept send and recv to perform
|
||||
deflate compression and decompression transparently.
|
||||
"""
|
||||
|
||||
def __init__(self, request):
|
||||
self._request = request
|
||||
self.connection = DeflateConnection(request.connection)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
if name in ('_request', 'connection'):
|
||||
return object.__getattribute__(self, name)
|
||||
return self._request.__getattribute__(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in ('_request', 'connection'):
|
||||
return object.__setattr__(self, name, value)
|
||||
return self._request.__setattr__(name, value)
|
||||
|
||||
|
||||
# By making wbits option negative, we can suppress CMF/FLG (2 octet) and
|
||||
# ADLER32 (4 octet) fields of zlib so that we can use zlib module just as
|
||||
# deflate library. DICTID won't be added as far as we don't set dictionary.
|
||||
# LZ77 window of 32K will be used for both compression and decompression.
|
||||
# For decompression, we can just use 32K to cover any windows size. For
|
||||
# compression, we use 32K so receivers must use 32K.
|
||||
#
|
||||
# Compression level is Z_DEFAULT_COMPRESSION. We don't have to match level
|
||||
# to decode.
|
||||
#
|
||||
# See zconf.h, deflate.cc, inflate.cc of zlib library, and zlibmodule.c of
|
||||
# Python. See also RFC1950 (ZLIB 3.3).
|
||||
class DeflateSocket(object):
|
||||
"""A wrapper class for socket object to intercept send and recv to perform
|
||||
deflate compression and decompression transparently.
|
||||
"""
|
||||
|
||||
# Size of the buffer passed to recv to receive compressed data.
|
||||
_RECV_SIZE = 4096
|
||||
|
||||
def __init__(self, socket):
|
||||
self._socket = socket
|
||||
|
||||
self._logger = logging.getLogger(
|
||||
'mod_pywebsocket.util.DeflateSocket')
|
||||
|
||||
self._compress = zlib.compressobj(
|
||||
zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
|
||||
self._decompress = zlib.decompressobj(-zlib.MAX_WBITS)
|
||||
self._unconsumed = ''
|
||||
|
||||
def recv(self, size):
|
||||
# TODO(tyoshino): Allow call with size=0. It should block until any
|
||||
# decompressed data is available.
|
||||
if size <= 0:
|
||||
raise Exception('Non-positive size passed')
|
||||
data = ''
|
||||
while True:
|
||||
data += self._decompress.decompress(
|
||||
self._unconsumed, size - len(data))
|
||||
self._unconsumed = self._decompress.unconsumed_tail
|
||||
if self._decompress.unused_data:
|
||||
raise Exception('Non-decompressible data found: %r' %
|
||||
self._decompress.unused_data)
|
||||
if len(data) != 0:
|
||||
break
|
||||
|
||||
read_data = self._socket.recv(DeflateSocket._RECV_SIZE)
|
||||
self._logger.debug('Received compressed: %r' % read_data)
|
||||
if not read_data:
|
||||
break
|
||||
self._unconsumed += read_data
|
||||
self._logger.debug('Received: %r' % data)
|
||||
return data
|
||||
|
||||
def sendall(self, bytes):
|
||||
self.send(bytes)
|
||||
|
||||
def send(self, bytes):
|
||||
compressed_bytes = self._compress.compress(bytes)
|
||||
compressed_bytes += self._compress.flush(zlib.Z_SYNC_FLUSH)
|
||||
self._socket.sendall(compressed_bytes)
|
||||
self._logger.debug('Wrote: %r' % bytes)
|
||||
self._logger.debug('Wrote compressed: %r' % compressed_bytes)
|
||||
return len(bytes)
|
||||
|
||||
|
||||
class DeflateConnection(object):
|
||||
"""A wrapper class for request object to intercept write and read to
|
||||
perform deflate compression and decompression transparently.
|
||||
"""
|
||||
|
||||
# Size of the buffer passed to recv to receive compressed data.
|
||||
_RECV_SIZE = 4096
|
||||
|
||||
def __init__(self, connection):
|
||||
self._connection = connection
|
||||
|
||||
self._logger = logging.getLogger(
|
||||
'mod_pywebsocket.util.DeflateConnection')
|
||||
|
||||
self._compress = zlib.compressobj(
|
||||
zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
|
||||
self._decompress = zlib.decompressobj(-zlib.MAX_WBITS)
|
||||
self._unconsumed = ''
|
||||
|
||||
def put_bytes(self, bytes):
|
||||
self.write(bytes)
|
||||
|
||||
def read(self, size=-1):
|
||||
# TODO(tyoshino): Allow call with size=0.
|
||||
if size == 0 or size < -1:
|
||||
raise Exception('size must be -1 or positive')
|
||||
|
||||
data = ''
|
||||
while True:
|
||||
if size < 0:
|
||||
data += self._decompress.decompress(self._unconsumed)
|
||||
else:
|
||||
data += self._decompress.decompress(
|
||||
self._unconsumed, size - len(data))
|
||||
self._unconsumed = self._decompress.unconsumed_tail
|
||||
if self._decompress.unused_data:
|
||||
raise Exception('Non-decompressible data found: %r' %
|
||||
self._decompress.unused_data)
|
||||
|
||||
if size >= 0 and len(data) != 0:
|
||||
break
|
||||
|
||||
# TODO(tyoshino): Make this read efficient by some workaround.
|
||||
#
|
||||
# In 3.0.3 and prior of mod_python, read blocks until length bytes
|
||||
# was read. We don't know the exact size to read while using
|
||||
# deflate, so read byte-by-byte.
|
||||
#
|
||||
# _StandaloneRequest.read that ultimately performs
|
||||
# socket._fileobject.read also blocks until length bytes was read
|
||||
read_data = self._connection.read(1)
|
||||
self._logger.debug('Read compressed: %r' % read_data)
|
||||
if not read_data:
|
||||
break
|
||||
self._unconsumed += read_data
|
||||
self._logger.debug('Read: %r' % data)
|
||||
return data
|
||||
|
||||
def write(self, bytes):
|
||||
compressed_bytes = self._compress.compress(bytes)
|
||||
compressed_bytes += self._compress.flush(zlib.Z_SYNC_FLUSH)
|
||||
self._logger.debug('Wrote compressed: %r' % compressed_bytes)
|
||||
self._logger.debug('Wrote: %r' % bytes)
|
||||
self._connection.write(compressed_bytes)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2009, Google Inc.
|
||||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
|
@ -30,7 +30,7 @@
|
|||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Standalone Web Socket server.
|
||||
"""Standalone WebSocket server.
|
||||
|
||||
Use this server to run mod_pywebsocket without Apache HTTP Server.
|
||||
|
||||
|
@ -45,8 +45,8 @@ Usage:
|
|||
|
||||
<document_root> is the path to the root directory of HTML files.
|
||||
|
||||
<websock_handlers> is the path to the root directory of Web Socket handlers.
|
||||
See __init__.py for details of <websock_handlers> and how to write Web Socket
|
||||
<websock_handlers> is the path to the root directory of WebSocket handlers.
|
||||
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
|
||||
|
@ -80,19 +80,13 @@ try:
|
|||
except ImportError:
|
||||
pass
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket import dispatch
|
||||
from mod_pywebsocket import handshake
|
||||
from mod_pywebsocket import memorizingfile
|
||||
from mod_pywebsocket import util
|
||||
|
||||
|
||||
_LOG_LEVELS = {
|
||||
'debug': logging.DEBUG,
|
||||
'info': logging.INFO,
|
||||
'warn': logging.WARN,
|
||||
'error': logging.ERROR,
|
||||
'critical': logging.CRITICAL};
|
||||
|
||||
_DEFAULT_LOG_MAX_BYTES = 1024 * 256
|
||||
_DEFAULT_LOG_BACKUP_COUNT = 5
|
||||
|
||||
|
@ -101,6 +95,7 @@ _DEFAULT_REQUEST_QUEUE_SIZE = 128
|
|||
# 1024 is practically large enough to contain WebSocket handshake lines.
|
||||
_MAX_MEMORIZED_LINES = 1024
|
||||
|
||||
|
||||
def _print_warnings_if_any(dispatcher):
|
||||
warnings = dispatcher.source_warnings()
|
||||
if warnings:
|
||||
|
@ -180,9 +175,10 @@ class _StandaloneRequest(object):
|
|||
|
||||
|
||||
class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
||||
"""HTTPServer specialized for Web Socket."""
|
||||
"""HTTPServer specialized for WebSocket."""
|
||||
|
||||
SocketServer.ThreadingMixIn.daemon_threads = True
|
||||
SocketServer.TCPServer.allow_reuse_address = True
|
||||
|
||||
def __init__(self, server_address, RequestHandlerClass):
|
||||
"""Override SocketServer.BaseServer.__init__."""
|
||||
|
@ -213,16 +209,21 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
|||
|
||||
|
||||
class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
|
||||
"""CGIHTTPRequestHandler specialized for Web Socket."""
|
||||
"""CGIHTTPRequestHandler specialized for WebSocket."""
|
||||
|
||||
def setup(self):
|
||||
"""Override SocketServer.StreamRequestHandler.setup."""
|
||||
"""Override SocketServer.StreamRequestHandler.setup to wrap rfile with
|
||||
MemorizingFile.
|
||||
"""
|
||||
|
||||
# Call superclass's setup to prepare rfile, wfile, etc. See setup
|
||||
# definition on the root class SocketServer.StreamRequestHandler to
|
||||
# understand what this does.
|
||||
CGIHTTPServer.CGIHTTPRequestHandler.setup(self)
|
||||
|
||||
self.connection = self.request
|
||||
self.rfile = memorizingfile.MemorizingFile(
|
||||
socket._fileobject(self.request, 'rb', self.rbufsize),
|
||||
max_memorized_lines=_MAX_MEMORIZED_LINES)
|
||||
self.wfile = socket._fileobject(self.request, 'wb', self.wbufsize)
|
||||
self.rfile,
|
||||
max_memorized_lines=_MAX_MEMORIZED_LINES)
|
||||
|
||||
def __init__(self, *args, **keywords):
|
||||
self._request = _StandaloneRequest(
|
||||
|
@ -258,6 +259,7 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
|
|||
# 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).
|
||||
|
@ -268,7 +270,7 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
|
|||
return False
|
||||
except Exception, e:
|
||||
logging.warning('mod_pywebsocket: %s' % e)
|
||||
logging.info('mod_pywebsocket: %s' % util.get_stack_trace())
|
||||
logging.warning('mod_pywebsocket: %s' % util.get_stack_trace())
|
||||
return False
|
||||
return result
|
||||
|
||||
|
@ -310,17 +312,18 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
|
|||
|
||||
def _configure_logging(options):
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(_LOG_LEVELS[options.log_level])
|
||||
logger.setLevel(logging.getLevelName(options.log_level.upper()))
|
||||
if options.log_file:
|
||||
handler = logging.handlers.RotatingFileHandler(
|
||||
options.log_file, 'a', options.log_max, options.log_count)
|
||||
else:
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter(
|
||||
"[%(asctime)s] [%(levelname)s] %(name)s: %(message)s")
|
||||
'[%(asctime)s] [%(levelname)s] %(name)s: %(message)s')
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
|
||||
def _alias_handlers(dispatcher, websock_handlers_map_file):
|
||||
"""Set aliases specified in websock_handler_map_file in dispatcher.
|
||||
|
||||
|
@ -346,7 +349,6 @@ def _alias_handlers(dispatcher, websock_handlers_map_file):
|
|||
fp.close()
|
||||
|
||||
|
||||
|
||||
def _main():
|
||||
parser = optparse.OptionParser()
|
||||
parser.add_option('-H', '--server-host', '--server_host',
|
||||
|
@ -354,22 +356,22 @@ def _main():
|
|||
default='',
|
||||
help='server hostname to listen to')
|
||||
parser.add_option('-p', '--port', dest='port', type='int',
|
||||
default=handshake.DEFAULT_WEB_SOCKET_PORT,
|
||||
default=common.DEFAULT_WEB_SOCKET_PORT,
|
||||
help='port to listen to')
|
||||
parser.add_option('-w', '--websock-handlers', '--websock_handlers',
|
||||
dest='websock_handlers',
|
||||
default='.',
|
||||
help='Web Socket handlers root directory.')
|
||||
help='WebSocket handlers root directory.')
|
||||
parser.add_option('-m', '--websock-handlers-map-file',
|
||||
'--websock_handlers_map_file',
|
||||
dest='websock_handlers_map_file',
|
||||
default=None,
|
||||
help=('Web Socket handlers map file. '
|
||||
help=('WebSocket handlers map file. '
|
||||
'Each line consists of alias_resource_path and '
|
||||
'existing_resource_path, separated by spaces.'))
|
||||
parser.add_option('-s', '--scan-dir', '--scan_dir', dest='scan_dir',
|
||||
default=None,
|
||||
help=('Web Socket handlers scan directory. '
|
||||
help=('WebSocket handlers scan directory. '
|
||||
'Must be a directory under websock_handlers.'))
|
||||
parser.add_option('-d', '--document-root', '--document_root',
|
||||
dest='document_root', default='.',
|
||||
|
@ -391,7 +393,8 @@ def _main():
|
|||
default='', help='Log file.')
|
||||
parser.add_option('--log-level', '--log_level', type='choice',
|
||||
dest='log_level', default='warn',
|
||||
choices=['debug', 'info', 'warn', 'error', 'critical'],
|
||||
choices=['debug', 'info', 'warning', 'warn', 'error',
|
||||
'critical'],
|
||||
help='Log level.')
|
||||
parser.add_option('--log-max', '--log_max', dest='log_max', type='int',
|
||||
default=_DEFAULT_LOG_MAX_BYTES,
|
||||
|
@ -461,7 +464,8 @@ def _main():
|
|||
WebSocketRequestHandler)
|
||||
server.serve_forever()
|
||||
except Exception, e:
|
||||
logging.critical(str(e))
|
||||
logging.critical('mod_pywebsocket: %s' % e)
|
||||
logging.critical('mod_pywebsocket: %s' % util.get_stack_trace())
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче