зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1654090 - Replace testing/mochitest/pywebsocket with pywebsocket3; r=jmaher
Update testing/mochitest/pywebsocket with the latest version available: pywebsocket3 is python 3 compatible. This keeps the basic structure of the old pywebsocket, but changes the directory name to pywebsocket3 to reflect the project renaming. Differential Revision: https://phabricator.services.mozilla.com/D84455
This commit is contained in:
Родитель
2c1c9b5c93
Коммит
217bbab384
2
.flake8
2
.flake8
|
@ -75,7 +75,7 @@ exclude =
|
|||
security/nss/,
|
||||
testing/marionette/harness/marionette_harness/runner/mixins,
|
||||
testing/marionette/harness/marionette_harness/tests,
|
||||
testing/mochitest/pywebsocket,
|
||||
testing/mochitest/pywebsocket3,
|
||||
testing/mozharness/configs/test/test_malformed.py,
|
||||
tools/lint/test/files,
|
||||
tools/infer/test/*.configure,
|
||||
|
|
|
@ -117,34 +117,28 @@ TEST_HARNESS_FILES.testing.mochitest.embed += [
|
|||
'embed/Xm5i5kbIXzc^headers^',
|
||||
]
|
||||
|
||||
TEST_HARNESS_FILES.testing.mochitest.pywebsocket += [
|
||||
'pywebsocket/standalone.py',
|
||||
TEST_HARNESS_FILES.testing.mochitest.pywebsocket3.mod_pywebsocket += [
|
||||
'pywebsocket3/mod_pywebsocket/__init__.py',
|
||||
'pywebsocket3/mod_pywebsocket/_stream_exceptions.py',
|
||||
'pywebsocket3/mod_pywebsocket/common.py',
|
||||
'pywebsocket3/mod_pywebsocket/dispatch.py',
|
||||
'pywebsocket3/mod_pywebsocket/extensions.py',
|
||||
'pywebsocket3/mod_pywebsocket/fast_masking.i',
|
||||
'pywebsocket3/mod_pywebsocket/http_header_util.py',
|
||||
'pywebsocket3/mod_pywebsocket/memorizingfile.py',
|
||||
'pywebsocket3/mod_pywebsocket/msgutil.py',
|
||||
'pywebsocket3/mod_pywebsocket/request_handler.py',
|
||||
'pywebsocket3/mod_pywebsocket/server_util.py',
|
||||
'pywebsocket3/mod_pywebsocket/standalone.py',
|
||||
'pywebsocket3/mod_pywebsocket/stream.py',
|
||||
'pywebsocket3/mod_pywebsocket/util.py',
|
||||
'pywebsocket3/mod_pywebsocket/websocket_server.py',
|
||||
]
|
||||
|
||||
TEST_HARNESS_FILES.testing.mochitest.pywebsocket.mod_pywebsocket += [
|
||||
'pywebsocket/mod_pywebsocket/__init__.py',
|
||||
'pywebsocket/mod_pywebsocket/_stream_base.py',
|
||||
'pywebsocket/mod_pywebsocket/_stream_hixie75.py',
|
||||
'pywebsocket/mod_pywebsocket/_stream_hybi.py',
|
||||
'pywebsocket/mod_pywebsocket/common.py',
|
||||
'pywebsocket/mod_pywebsocket/dispatch.py',
|
||||
'pywebsocket/mod_pywebsocket/extensions.py',
|
||||
'pywebsocket/mod_pywebsocket/fast_masking.i',
|
||||
'pywebsocket/mod_pywebsocket/headerparserhandler.py',
|
||||
'pywebsocket/mod_pywebsocket/http_header_util.py',
|
||||
'pywebsocket/mod_pywebsocket/memorizingfile.py',
|
||||
'pywebsocket/mod_pywebsocket/msgutil.py',
|
||||
'pywebsocket/mod_pywebsocket/mux.py',
|
||||
'pywebsocket/mod_pywebsocket/stream.py',
|
||||
'pywebsocket/mod_pywebsocket/util.py',
|
||||
'pywebsocket/mod_pywebsocket/xhr_benchmark_handler.py',
|
||||
]
|
||||
|
||||
TEST_HARNESS_FILES.testing.mochitest.pywebsocket.mod_pywebsocket.handshake += [
|
||||
'pywebsocket/mod_pywebsocket/handshake/__init__.py',
|
||||
'pywebsocket/mod_pywebsocket/handshake/_base.py',
|
||||
'pywebsocket/mod_pywebsocket/handshake/hybi.py',
|
||||
'pywebsocket/mod_pywebsocket/handshake/hybi00.py',
|
||||
TEST_HARNESS_FILES.testing.mochitest.pywebsocket3.mod_pywebsocket.handshake += [
|
||||
'pywebsocket3/mod_pywebsocket/handshake/__init__.py',
|
||||
'pywebsocket3/mod_pywebsocket/handshake/_base.py',
|
||||
'pywebsocket3/mod_pywebsocket/handshake/hybi.py',
|
||||
]
|
||||
|
||||
TEST_HARNESS_FILES.testing.mochitest.dynamic += [
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
INSTALL
|
||||
|
||||
To install this package to the system, run this:
|
||||
$ python setup.py build
|
||||
$ sudo python setup.py install
|
||||
|
||||
To install this package as a normal user, run this instead:
|
||||
$ python setup.py build
|
||||
$ python setup.py install --user
|
||||
|
||||
LAUNCH
|
||||
|
||||
To use pywebsocket as Apache module, run this to read the document:
|
||||
$ pydoc mod_pywebsocket
|
||||
|
||||
To use pywebsocket as standalone server, run this to read the document:
|
||||
$ pydoc mod_pywebsocket.standalone
|
|
@ -1,96 +0,0 @@
|
|||
This pywebsocket code is mostly unchanged from the source at
|
||||
|
||||
svn checkout http://pywebsocket.googlecode.com/svn/trunk/ pywebsocket-read-only
|
||||
|
||||
The current Mozilla code is based on
|
||||
|
||||
svnversion: 860 (supports RFC 6455, permessage compression extension)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
STEPS TO UPDATE MOZILLA TO NEWER PYWEBSOCKET VERSION
|
||||
--------------------------------------------------------------------------------
|
||||
- Get new pywebsocket checkout from googlecode (into, for instance, 'src')
|
||||
|
||||
svn checkout http://pywebsocket.googlecode.com/svn/trunk/ pywebsocket-read-only
|
||||
|
||||
- Export a version w/o SVN files:
|
||||
|
||||
svn export src dist
|
||||
|
||||
- rsync new version into our tree, deleting files that aren't needed any more
|
||||
(NOTE: this will blow away this file! hg revert it or keep a copy.)
|
||||
|
||||
rsync -rv --delete dist/ $MOZ_SRC/testing/mochitest/pywebsocket
|
||||
|
||||
- Get rid of examples/test directory and some cruft:
|
||||
|
||||
rm -rf example test setup.py MANIFEST.in
|
||||
|
||||
- Manually move the 'standalone.py' file from the mmod_pywebsocket/ directory to
|
||||
the parent directory (not sure why we moved it: probably no reason)
|
||||
|
||||
- hg add/rm appropriate files, and add/remove them from
|
||||
testing/mochitest/moz.build
|
||||
|
||||
- We need to apply the patch to hybi.py that makes HSTS work: (attached at end
|
||||
of this README)
|
||||
|
||||
- Test and make sure the code works:
|
||||
|
||||
make mochitest-plain TEST_PATH=dom/base/test/test_websocket.html
|
||||
|
||||
- If this doesn't take a look at the pywebsocket server log,
|
||||
$OBJDIR/_tests/testing/mochitest/websock.log
|
||||
|
||||
- Upgrade the svnversion number at top of this file to whatever version we're
|
||||
now based off of.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
PATCH TO hybi.py for HSTS support:
|
||||
|
||||
|
||||
diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py
|
||||
--- a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py
|
||||
+++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py
|
||||
@@ -299,16 +299,19 @@ class Handshaker(object):
|
||||
status=common.HTTP_STATUS_BAD_REQUEST)
|
||||
raise VersionException(
|
||||
'Unsupported version %r for header %s' %
|
||||
(version, common.SEC_WEBSOCKET_VERSION_HEADER),
|
||||
supported_versions=', '.join(map(str, _SUPPORTED_VERSIONS)))
|
||||
|
||||
def _set_protocol(self):
|
||||
self._request.ws_protocol = None
|
||||
+ # MOZILLA
|
||||
+ self._request.sts = None
|
||||
+ # /MOZILLA
|
||||
|
||||
protocol_header = self._request.headers_in.get(
|
||||
common.SEC_WEBSOCKET_PROTOCOL_HEADER)
|
||||
|
||||
if protocol_header is None:
|
||||
self._request.ws_requested_protocols = None
|
||||
return
|
||||
|
||||
@@ -396,16 +399,21 @@ class Handshaker(object):
|
||||
response.append(format_header(
|
||||
common.SEC_WEBSOCKET_PROTOCOL_HEADER,
|
||||
self._request.ws_protocol))
|
||||
if (self._request.ws_extensions is not None and
|
||||
len(self._request.ws_extensions) != 0):
|
||||
response.append(format_header(
|
||||
common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
|
||||
common.format_extensions(self._request.ws_extensions)))
|
||||
+ # MOZILLA: Add HSTS header if requested to
|
||||
+ if self._request.sts is not None:
|
||||
+ response.append(format_header("Strict-Transport-Security",
|
||||
+ self._request.sts))
|
||||
+ # /MOZILLA
|
||||
|
||||
# Headers not specific for WebSocket
|
||||
for name, value in self._request.extra_headers:
|
||||
response.append(format_header(name, value))
|
||||
|
||||
response.append('\r\n')
|
||||
|
||||
return ''.join(response)
|
|
@ -1,181 +0,0 @@
|
|||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Base stream class.
|
||||
"""
|
||||
|
||||
|
||||
# Note: request.connection.write/read are used in this module, even though
|
||||
# mod_python document says that they should be used only in connection
|
||||
# handlers. Unfortunately, we have no other options. For example,
|
||||
# request.write/read are not suitable because they don't allow direct raw bytes
|
||||
# writing/reading.
|
||||
|
||||
|
||||
import socket
|
||||
|
||||
from mod_pywebsocket import util
|
||||
|
||||
|
||||
# Exceptions
|
||||
|
||||
|
||||
class ConnectionTerminatedException(Exception):
|
||||
"""This exception will be raised when a connection is terminated
|
||||
unexpectedly.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidFrameException(ConnectionTerminatedException):
|
||||
"""This exception will be raised when we received an invalid frame we
|
||||
cannot parse.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BadOperationException(Exception):
|
||||
"""This exception will be raised when send_message() is called on
|
||||
server-terminated connection or receive_message() is called on
|
||||
client-terminated connection.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedFrameException(Exception):
|
||||
"""This exception will be raised when we receive a frame with flag, opcode
|
||||
we cannot handle. Handlers can just catch and ignore this exception and
|
||||
call receive_message() again to continue processing the next frame.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidUTF8Exception(Exception):
|
||||
"""This exception will be raised when we receive a text frame which
|
||||
contains invalid UTF-8 strings.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class StreamBase(object):
|
||||
"""Base stream class."""
|
||||
|
||||
def __init__(self, request):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
"""
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._request = request
|
||||
|
||||
def _read(self, length):
|
||||
"""Reads length bytes from connection. In case we catch any exception,
|
||||
prepends remote address to the exception message and raise again.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty string.
|
||||
"""
|
||||
|
||||
try:
|
||||
read_bytes = self._request.connection.read(length)
|
||||
if not read_bytes:
|
||||
raise ConnectionTerminatedException(
|
||||
'Receiving %d byte failed. Peer (%r) closed connection' %
|
||||
(length, (self._request.connection.remote_addr,)))
|
||||
return read_bytes
|
||||
except socket.error, e:
|
||||
# Catch a socket.error. Because it's not a child class of the
|
||||
# IOError prior to Python 2.6, we cannot omit this except clause.
|
||||
# Use %s rather than %r for the exception to use human friendly
|
||||
# format.
|
||||
raise ConnectionTerminatedException(
|
||||
'Receiving %d byte failed. socket.error (%s) occurred' %
|
||||
(length, e))
|
||||
except IOError, e:
|
||||
# Also catch an IOError because mod_python throws it.
|
||||
raise ConnectionTerminatedException(
|
||||
'Receiving %d byte failed. IOError (%s) occurred' %
|
||||
(length, e))
|
||||
|
||||
def _write(self, bytes_to_write):
|
||||
"""Writes given bytes to connection. In case we catch any exception,
|
||||
prepends remote address to the exception message and raise again.
|
||||
"""
|
||||
|
||||
try:
|
||||
self._request.connection.write(bytes_to_write)
|
||||
except Exception, e:
|
||||
util.prepend_message_to_exception(
|
||||
'Failed to send message to %r: ' %
|
||||
(self._request.connection.remote_addr,),
|
||||
e)
|
||||
raise
|
||||
|
||||
def receive_bytes(self, length):
|
||||
"""Receives multiple bytes. Retries read when we couldn't receive the
|
||||
specified amount.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty string.
|
||||
"""
|
||||
|
||||
read_bytes = []
|
||||
while length > 0:
|
||||
new_read_bytes = self._read(length)
|
||||
read_bytes.append(new_read_bytes)
|
||||
length -= len(new_read_bytes)
|
||||
return ''.join(read_bytes)
|
||||
|
||||
def _read_until(self, delim_char):
|
||||
"""Reads bytes until we encounter delim_char. The result will not
|
||||
contain delim_char.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty string.
|
||||
"""
|
||||
|
||||
read_bytes = []
|
||||
while True:
|
||||
ch = self._read(1)
|
||||
if ch == delim_char:
|
||||
break
|
||||
read_bytes.append(ch)
|
||||
return ''.join(read_bytes)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -1,229 +0,0 @@
|
|||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""This file provides a class for parsing/building frames of the WebSocket
|
||||
protocol version HyBi 00 and Hixie 75.
|
||||
|
||||
Specification:
|
||||
- HyBi 00 http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00
|
||||
- Hixie 75 http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
|
||||
"""
|
||||
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket._stream_base import BadOperationException
|
||||
from mod_pywebsocket._stream_base import ConnectionTerminatedException
|
||||
from mod_pywebsocket._stream_base import InvalidFrameException
|
||||
from mod_pywebsocket._stream_base import StreamBase
|
||||
from mod_pywebsocket._stream_base import UnsupportedFrameException
|
||||
from mod_pywebsocket import util
|
||||
|
||||
|
||||
class StreamHixie75(StreamBase):
|
||||
"""A class for parsing/building frames of the WebSocket protocol version
|
||||
HyBi 00 and Hixie 75.
|
||||
"""
|
||||
|
||||
def __init__(self, request, enable_closing_handshake=False):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
enable_closing_handshake: to let StreamHixie75 perform closing
|
||||
handshake as specified in HyBi 00, set
|
||||
this option to True.
|
||||
"""
|
||||
|
||||
StreamBase.__init__(self, request)
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._enable_closing_handshake = enable_closing_handshake
|
||||
|
||||
self._request.client_terminated = False
|
||||
self._request.server_terminated = False
|
||||
|
||||
def send_message(self, message, end=True, binary=False):
|
||||
"""Send message.
|
||||
|
||||
Args:
|
||||
message: unicode string to send.
|
||||
binary: not used in hixie75.
|
||||
|
||||
Raises:
|
||||
BadOperationException: when called on a server-terminated
|
||||
connection.
|
||||
"""
|
||||
|
||||
if not end:
|
||||
raise BadOperationException(
|
||||
'StreamHixie75 doesn\'t support send_message with end=False')
|
||||
|
||||
if binary:
|
||||
raise BadOperationException(
|
||||
'StreamHixie75 doesn\'t support send_message with binary=True')
|
||||
|
||||
if self._request.server_terminated:
|
||||
raise BadOperationException(
|
||||
'Requested send_message after sending out a closing handshake')
|
||||
|
||||
self._write(''.join(['\x00', message.encode('utf-8'), '\xff']))
|
||||
|
||||
def _read_payload_length_hixie75(self):
|
||||
"""Reads a length header in a Hixie75 version frame with length.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty string.
|
||||
"""
|
||||
|
||||
length = 0
|
||||
while True:
|
||||
b_str = self._read(1)
|
||||
b = ord(b_str)
|
||||
length = length * 128 + (b & 0x7f)
|
||||
if (b & 0x80) == 0:
|
||||
break
|
||||
return length
|
||||
|
||||
def receive_message(self):
|
||||
"""Receive a WebSocket frame and return its payload an unicode string.
|
||||
|
||||
Returns:
|
||||
payload unicode string in a WebSocket frame.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty
|
||||
string.
|
||||
BadOperationException: when called on a client-terminated
|
||||
connection.
|
||||
"""
|
||||
|
||||
if self._request.client_terminated:
|
||||
raise BadOperationException(
|
||||
'Requested receive_message after receiving a closing '
|
||||
'handshake')
|
||||
|
||||
while True:
|
||||
# Read 1 byte.
|
||||
# mp_conn.read will block if no bytes are available.
|
||||
# Timeout is controlled by TimeOut directive of Apache.
|
||||
frame_type_str = self.receive_bytes(1)
|
||||
frame_type = ord(frame_type_str)
|
||||
if (frame_type & 0x80) == 0x80:
|
||||
# The payload length is specified in the frame.
|
||||
# Read and discard.
|
||||
length = self._read_payload_length_hixie75()
|
||||
if length > 0:
|
||||
_ = self.receive_bytes(length)
|
||||
# 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the
|
||||
# /client terminated/ flag and abort these steps.
|
||||
if not self._enable_closing_handshake:
|
||||
continue
|
||||
|
||||
if frame_type == 0xFF and length == 0:
|
||||
self._request.client_terminated = True
|
||||
|
||||
if self._request.server_terminated:
|
||||
self._logger.debug(
|
||||
'Received ack for server-initiated closing '
|
||||
'handshake')
|
||||
return None
|
||||
|
||||
self._logger.debug(
|
||||
'Received client-initiated closing handshake')
|
||||
|
||||
self._send_closing_handshake()
|
||||
self._logger.debug(
|
||||
'Sent ack for client-initiated closing handshake')
|
||||
return None
|
||||
else:
|
||||
# The payload is delimited with \xff.
|
||||
bytes = self._read_until('\xff')
|
||||
# The WebSocket protocol section 4.4 specifies that invalid
|
||||
# characters must be replaced with U+fffd REPLACEMENT
|
||||
# CHARACTER.
|
||||
message = bytes.decode('utf-8', 'replace')
|
||||
if frame_type == 0x00:
|
||||
return message
|
||||
# Discard data of other types.
|
||||
|
||||
def _send_closing_handshake(self):
|
||||
if not self._enable_closing_handshake:
|
||||
raise BadOperationException(
|
||||
'Closing handshake is not supported in Hixie 75 protocol')
|
||||
|
||||
self._request.server_terminated = True
|
||||
|
||||
# 5.3 the server may decide to terminate the WebSocket connection by
|
||||
# running through the following steps:
|
||||
# 1. send a 0xFF byte and a 0x00 byte to the client to indicate the
|
||||
# start of the closing handshake.
|
||||
self._write('\xff\x00')
|
||||
|
||||
def close_connection(self, unused_code='', unused_reason=''):
|
||||
"""Closes a WebSocket connection.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when closing handshake was
|
||||
not successfull.
|
||||
"""
|
||||
|
||||
if self._request.server_terminated:
|
||||
self._logger.debug(
|
||||
'Requested close_connection but server is already terminated')
|
||||
return
|
||||
|
||||
if not self._enable_closing_handshake:
|
||||
self._request.server_terminated = True
|
||||
self._logger.debug('Connection closed')
|
||||
return
|
||||
|
||||
self._send_closing_handshake()
|
||||
self._logger.debug('Sent server-initiated closing handshake')
|
||||
|
||||
# TODO(ukai): 2. wait until the /client terminated/ flag has been set,
|
||||
# or until a server-defined timeout expires.
|
||||
#
|
||||
# For now, we expect receiving closing handshake right after sending
|
||||
# out closing handshake, and if we couldn't receive non-handshake
|
||||
# frame, we take it as ConnectionTerminatedException.
|
||||
message = self.receive_message()
|
||||
if message is not None:
|
||||
raise ConnectionTerminatedException(
|
||||
'Didn\'t receive valid ack for closing handshake')
|
||||
# TODO: 3. close the WebSocket connection.
|
||||
# note: mod_python Connection (mp_conn) doesn't have close method.
|
||||
|
||||
def send_ping(self, body):
|
||||
raise BadOperationException(
|
||||
'StreamHixie75 doesn\'t support send_ping')
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -1,293 +0,0 @@
|
|||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""This file provides the opening handshake processor for the WebSocket
|
||||
protocol version HyBi 00.
|
||||
|
||||
Specification:
|
||||
http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00
|
||||
"""
|
||||
|
||||
|
||||
# Note: request.connection.write/read are used in this module, even though
|
||||
# mod_python document says that they should be used only in connection
|
||||
# handlers. Unfortunately, we have no other options. For example,
|
||||
# request.write/read are not suitable because they don't allow direct raw bytes
|
||||
# writing/reading.
|
||||
|
||||
|
||||
import logging
|
||||
import re
|
||||
import struct
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket.stream import StreamHixie75
|
||||
from mod_pywebsocket import util
|
||||
from mod_pywebsocket.handshake._base import HandshakeException
|
||||
from mod_pywebsocket.handshake._base import check_request_line
|
||||
from mod_pywebsocket.handshake._base import format_header
|
||||
from mod_pywebsocket.handshake._base import get_default_port
|
||||
from mod_pywebsocket.handshake._base import get_mandatory_header
|
||||
from mod_pywebsocket.handshake._base import parse_host_header
|
||||
from mod_pywebsocket.handshake._base import validate_mandatory_header
|
||||
|
||||
|
||||
_MANDATORY_HEADERS = [
|
||||
# key, expected value or None
|
||||
[common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75],
|
||||
[common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE],
|
||||
]
|
||||
|
||||
|
||||
def _validate_subprotocol(subprotocol):
|
||||
"""Checks if characters in subprotocol are in range between U+0020 and
|
||||
U+007E. A value in the Sec-WebSocket-Protocol field need to satisfy this
|
||||
requirement.
|
||||
|
||||
See the Section 4.1. Opening handshake of the spec.
|
||||
"""
|
||||
|
||||
if not subprotocol:
|
||||
raise HandshakeException('Invalid subprotocol name: empty')
|
||||
|
||||
# Parameter should be in the range U+0020 to U+007E.
|
||||
for c in subprotocol:
|
||||
if not 0x20 <= ord(c) <= 0x7e:
|
||||
raise HandshakeException(
|
||||
'Illegal character in subprotocol name: %r' % c)
|
||||
|
||||
|
||||
def _check_header_lines(request, mandatory_headers):
|
||||
check_request_line(request)
|
||||
|
||||
# The expected field names, and the meaning of their corresponding
|
||||
# values, are as follows.
|
||||
# |Upgrade| and |Connection|
|
||||
for key, expected_value in mandatory_headers:
|
||||
validate_mandatory_header(request, key, expected_value)
|
||||
|
||||
|
||||
def _build_location(request):
|
||||
"""Build WebSocket location for request."""
|
||||
|
||||
location_parts = []
|
||||
if request.is_https():
|
||||
location_parts.append(common.WEB_SOCKET_SECURE_SCHEME)
|
||||
else:
|
||||
location_parts.append(common.WEB_SOCKET_SCHEME)
|
||||
location_parts.append('://')
|
||||
host, port = parse_host_header(request)
|
||||
connection_port = request.connection.local_addr[1]
|
||||
if port != connection_port:
|
||||
raise HandshakeException('Header/connection port mismatch: %d/%d' %
|
||||
(port, connection_port))
|
||||
location_parts.append(host)
|
||||
if (port != get_default_port(request.is_https())):
|
||||
location_parts.append(':')
|
||||
location_parts.append(str(port))
|
||||
location_parts.append(request.unparsed_uri)
|
||||
return ''.join(location_parts)
|
||||
|
||||
|
||||
class Handshaker(object):
|
||||
"""Opening handshake processor for the WebSocket protocol version HyBi 00.
|
||||
"""
|
||||
|
||||
def __init__(self, request, dispatcher):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
dispatcher: Dispatcher (dispatch.Dispatcher).
|
||||
|
||||
Handshaker will add attributes such as ws_resource in performing
|
||||
handshake.
|
||||
"""
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._request = request
|
||||
self._dispatcher = dispatcher
|
||||
|
||||
def do_handshake(self):
|
||||
"""Perform WebSocket Handshake.
|
||||
|
||||
On _request, we set
|
||||
ws_resource, ws_protocol, ws_location, ws_origin, ws_challenge,
|
||||
ws_challenge_md5: WebSocket handshake information.
|
||||
ws_stream: Frame generation/parsing class.
|
||||
ws_version: Protocol version.
|
||||
|
||||
Raises:
|
||||
HandshakeException: when any error happened in parsing the opening
|
||||
handshake request.
|
||||
"""
|
||||
|
||||
# 5.1 Reading the client's opening handshake.
|
||||
# dispatcher sets it in self._request.
|
||||
_check_header_lines(self._request, _MANDATORY_HEADERS)
|
||||
self._set_resource()
|
||||
self._set_subprotocol()
|
||||
self._set_location()
|
||||
self._set_origin()
|
||||
self._set_challenge_response()
|
||||
self._set_protocol_version()
|
||||
|
||||
self._dispatcher.do_extra_handshake(self._request)
|
||||
|
||||
self._send_handshake()
|
||||
|
||||
def _set_resource(self):
|
||||
self._request.ws_resource = self._request.uri
|
||||
|
||||
def _set_subprotocol(self):
|
||||
# |Sec-WebSocket-Protocol|
|
||||
subprotocol = self._request.headers_in.get(
|
||||
common.SEC_WEBSOCKET_PROTOCOL_HEADER)
|
||||
if subprotocol is not None:
|
||||
_validate_subprotocol(subprotocol)
|
||||
self._request.ws_protocol = subprotocol
|
||||
|
||||
def _set_location(self):
|
||||
# |Host|
|
||||
host = self._request.headers_in.get(common.HOST_HEADER)
|
||||
if host is not None:
|
||||
self._request.ws_location = _build_location(self._request)
|
||||
# TODO(ukai): check host is this host.
|
||||
|
||||
def _set_origin(self):
|
||||
# |Origin|
|
||||
origin = self._request.headers_in.get(common.ORIGIN_HEADER)
|
||||
if origin is not None:
|
||||
self._request.ws_origin = origin
|
||||
|
||||
def _set_protocol_version(self):
|
||||
# |Sec-WebSocket-Draft|
|
||||
draft = self._request.headers_in.get(common.SEC_WEBSOCKET_DRAFT_HEADER)
|
||||
if draft is not None and draft != '0':
|
||||
raise HandshakeException('Illegal value for %s: %s' %
|
||||
(common.SEC_WEBSOCKET_DRAFT_HEADER,
|
||||
draft))
|
||||
|
||||
self._logger.debug('Protocol version is HyBi 00')
|
||||
self._request.ws_version = common.VERSION_HYBI00
|
||||
self._request.ws_stream = StreamHixie75(self._request, True)
|
||||
|
||||
def _set_challenge_response(self):
|
||||
# 5.2 4-8.
|
||||
self._request.ws_challenge = self._get_challenge()
|
||||
# 5.2 9. let /response/ be the MD5 finterprint of /challenge/
|
||||
self._request.ws_challenge_md5 = util.md5_hash(
|
||||
self._request.ws_challenge).digest()
|
||||
self._logger.debug(
|
||||
'Challenge: %r (%s)',
|
||||
self._request.ws_challenge,
|
||||
util.hexify(self._request.ws_challenge))
|
||||
self._logger.debug(
|
||||
'Challenge response: %r (%s)',
|
||||
self._request.ws_challenge_md5,
|
||||
util.hexify(self._request.ws_challenge_md5))
|
||||
|
||||
def _get_key_value(self, key_field):
|
||||
key_value = get_mandatory_header(self._request, key_field)
|
||||
|
||||
self._logger.debug('%s: %r', key_field, key_value)
|
||||
|
||||
# 5.2 4. let /key-number_n/ be the digits (characters in the range
|
||||
# U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9)) in /key_n/,
|
||||
# interpreted as a base ten integer, ignoring all other characters
|
||||
# in /key_n/.
|
||||
try:
|
||||
key_number = int(re.sub("\\D", "", key_value))
|
||||
except:
|
||||
raise HandshakeException('%s field contains no digit' % key_field)
|
||||
# 5.2 5. let /spaces_n/ be the number of U+0020 SPACE characters
|
||||
# in /key_n/.
|
||||
spaces = re.subn(" ", "", key_value)[1]
|
||||
if spaces == 0:
|
||||
raise HandshakeException('%s field contains no space' % key_field)
|
||||
|
||||
self._logger.debug(
|
||||
'%s: Key-number is %d and number of spaces is %d',
|
||||
key_field, key_number, spaces)
|
||||
|
||||
# 5.2 6. if /key-number_n/ is not an integral multiple of /spaces_n/
|
||||
# then abort the WebSocket connection.
|
||||
if key_number % spaces != 0:
|
||||
raise HandshakeException(
|
||||
'%s: Key-number (%d) is not an integral multiple of spaces '
|
||||
'(%d)' % (key_field, key_number, spaces))
|
||||
# 5.2 7. let /part_n/ be /key-number_n/ divided by /spaces_n/.
|
||||
part = key_number / spaces
|
||||
self._logger.debug('%s: Part is %d', key_field, part)
|
||||
return part
|
||||
|
||||
def _get_challenge(self):
|
||||
# 5.2 4-7.
|
||||
key1 = self._get_key_value(common.SEC_WEBSOCKET_KEY1_HEADER)
|
||||
key2 = self._get_key_value(common.SEC_WEBSOCKET_KEY2_HEADER)
|
||||
# 5.2 8. let /challenge/ be the concatenation of /part_1/,
|
||||
challenge = ''
|
||||
challenge += struct.pack('!I', key1) # network byteorder int
|
||||
challenge += struct.pack('!I', key2) # network byteorder int
|
||||
challenge += self._request.connection.read(8)
|
||||
return challenge
|
||||
|
||||
def _send_handshake(self):
|
||||
response = []
|
||||
|
||||
# 5.2 10. send the following line.
|
||||
response.append('HTTP/1.1 101 WebSocket Protocol Handshake\r\n')
|
||||
|
||||
# 5.2 11. send the following fields to the client.
|
||||
response.append(format_header(
|
||||
common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75))
|
||||
response.append(format_header(
|
||||
common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE))
|
||||
response.append(format_header(
|
||||
common.SEC_WEBSOCKET_LOCATION_HEADER, self._request.ws_location))
|
||||
response.append(format_header(
|
||||
common.SEC_WEBSOCKET_ORIGIN_HEADER, self._request.ws_origin))
|
||||
if self._request.ws_protocol:
|
||||
response.append(format_header(
|
||||
common.SEC_WEBSOCKET_PROTOCOL_HEADER,
|
||||
self._request.ws_protocol))
|
||||
# 5.2 12. send two bytes 0x0D 0x0A.
|
||||
response.append('\r\n')
|
||||
# 5.2 13. send /response/
|
||||
response.append(self._request.ws_challenge_md5)
|
||||
|
||||
raw_response = ''.join(response)
|
||||
self._request.connection.write(raw_response)
|
||||
self._logger.debug('Sent server\'s opening handshake: %r',
|
||||
raw_response)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -1,254 +0,0 @@
|
|||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""PythonHeaderParserHandler for mod_pywebsocket.
|
||||
|
||||
Apache HTTP Server and mod_python must be configured such that this
|
||||
function is called to handle WebSocket request.
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
from mod_python import apache
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket import dispatch
|
||||
from mod_pywebsocket import handshake
|
||||
from mod_pywebsocket import util
|
||||
|
||||
|
||||
# PythonOption to specify the handler root directory.
|
||||
_PYOPT_HANDLER_ROOT = 'mod_pywebsocket.handler_root'
|
||||
|
||||
# PythonOption to specify the handler scan directory.
|
||||
# This must be a directory under the root directory.
|
||||
# The default is the root directory.
|
||||
_PYOPT_HANDLER_SCAN = 'mod_pywebsocket.handler_scan'
|
||||
|
||||
# PythonOption to allow handlers whose canonical path is
|
||||
# not under the root directory. It's disallowed by default.
|
||||
# Set this option with value of 'yes' to allow.
|
||||
_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT = (
|
||||
'mod_pywebsocket.allow_handlers_outside_root_dir')
|
||||
# Map from values to their meanings. 'Yes' and 'No' are allowed just for
|
||||
# compatibility.
|
||||
_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT_DEFINITION = {
|
||||
'off': False, 'no': False, 'on': True, 'yes': True}
|
||||
|
||||
# (Obsolete option. Ignored.)
|
||||
# PythonOption to specify to allow handshake defined in Hixie 75 version
|
||||
# protocol. The default is None (Off)
|
||||
_PYOPT_ALLOW_DRAFT75 = 'mod_pywebsocket.allow_draft75'
|
||||
# Map from values to their meanings.
|
||||
_PYOPT_ALLOW_DRAFT75_DEFINITION = {'off': False, 'on': True}
|
||||
|
||||
|
||||
class ApacheLogHandler(logging.Handler):
|
||||
"""Wrapper logging.Handler to emit log message to apache's error.log."""
|
||||
|
||||
_LEVELS = {
|
||||
logging.DEBUG: apache.APLOG_DEBUG,
|
||||
logging.INFO: apache.APLOG_INFO,
|
||||
logging.WARNING: apache.APLOG_WARNING,
|
||||
logging.ERROR: apache.APLOG_ERR,
|
||||
logging.CRITICAL: apache.APLOG_CRIT,
|
||||
}
|
||||
|
||||
def __init__(self, request=None):
|
||||
logging.Handler.__init__(self)
|
||||
self._log_error = apache.log_error
|
||||
if request is not None:
|
||||
self._log_error = request.log_error
|
||||
|
||||
# Time and level will be printed by Apache.
|
||||
self._formatter = logging.Formatter('%(name)s: %(message)s')
|
||||
|
||||
def emit(self, record):
|
||||
apache_level = apache.APLOG_DEBUG
|
||||
if record.levelno in ApacheLogHandler._LEVELS:
|
||||
apache_level = ApacheLogHandler._LEVELS[record.levelno]
|
||||
|
||||
msg = self._formatter.format(record)
|
||||
|
||||
# "server" parameter must be passed to have "level" parameter work.
|
||||
# If only "level" parameter is passed, nothing shows up on Apache's
|
||||
# log. However, at this point, we cannot get the server object of the
|
||||
# virtual host which will process WebSocket requests. The only server
|
||||
# object we can get here is apache.main_server. But Wherever (server
|
||||
# configuration context or virtual host context) we put
|
||||
# PythonHeaderParserHandler directive, apache.main_server just points
|
||||
# the main server instance (not any of virtual server instance). Then,
|
||||
# Apache follows LogLevel directive in the server configuration context
|
||||
# to filter logs. So, we need to specify LogLevel in the server
|
||||
# configuration context. Even if we specify "LogLevel debug" in the
|
||||
# virtual host context which actually handles WebSocket connections,
|
||||
# DEBUG level logs never show up unless "LogLevel debug" is specified
|
||||
# in the server configuration context.
|
||||
#
|
||||
# TODO(tyoshino): Provide logging methods on request object. When
|
||||
# request is mp_request object (when used together with Apache), the
|
||||
# methods call request.log_error indirectly. When request is
|
||||
# _StandaloneRequest, the methods call Python's logging facility which
|
||||
# we create in standalone.py.
|
||||
self._log_error(msg, apache_level, apache.main_server)
|
||||
|
||||
|
||||
def _configure_logging():
|
||||
logger = logging.getLogger()
|
||||
# Logs are filtered by Apache based on LogLevel directive in Apache
|
||||
# configuration file. We must just pass logs for all levels to
|
||||
# ApacheLogHandler.
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logger.addHandler(ApacheLogHandler())
|
||||
|
||||
|
||||
_configure_logging()
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _parse_option(name, value, definition):
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
meaning = definition.get(value.lower())
|
||||
if meaning is None:
|
||||
raise Exception('Invalid value for PythonOption %s: %r' %
|
||||
(name, value))
|
||||
return meaning
|
||||
|
||||
|
||||
def _create_dispatcher():
|
||||
_LOGGER.info('Initializing Dispatcher')
|
||||
|
||||
options = apache.main_server.get_options()
|
||||
|
||||
handler_root = options.get(_PYOPT_HANDLER_ROOT, None)
|
||||
if not handler_root:
|
||||
raise Exception('PythonOption %s is not defined' % _PYOPT_HANDLER_ROOT,
|
||||
apache.APLOG_ERR)
|
||||
|
||||
handler_scan = options.get(_PYOPT_HANDLER_SCAN, handler_root)
|
||||
|
||||
allow_handlers_outside_root = _parse_option(
|
||||
_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT,
|
||||
options.get(_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT),
|
||||
_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT_DEFINITION)
|
||||
|
||||
dispatcher = dispatch.Dispatcher(
|
||||
handler_root, handler_scan, allow_handlers_outside_root)
|
||||
|
||||
for warning in dispatcher.source_warnings():
|
||||
apache.log_error(
|
||||
'mod_pywebsocket: Warning in source loading: %s' % warning,
|
||||
apache.APLOG_WARNING)
|
||||
|
||||
return dispatcher
|
||||
|
||||
|
||||
# Initialize
|
||||
_dispatcher = _create_dispatcher()
|
||||
|
||||
|
||||
def headerparserhandler(request):
|
||||
"""Handle request.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
|
||||
This function is named headerparserhandler because it is the default
|
||||
name for a PythonHeaderParserHandler.
|
||||
"""
|
||||
|
||||
handshake_is_done = False
|
||||
try:
|
||||
# Fallback to default http handler for request paths for which
|
||||
# we don't have request handlers.
|
||||
if not _dispatcher.get_handler_suite(request.uri):
|
||||
request.log_error(
|
||||
'mod_pywebsocket: No handler for resource: %r' % request.uri,
|
||||
apache.APLOG_INFO)
|
||||
request.log_error(
|
||||
'mod_pywebsocket: Fallback to Apache', apache.APLOG_INFO)
|
||||
return apache.DECLINED
|
||||
except dispatch.DispatchException, e:
|
||||
request.log_error(
|
||||
'mod_pywebsocket: Dispatch failed for error: %s' % e,
|
||||
apache.APLOG_INFO)
|
||||
if not handshake_is_done:
|
||||
return e.status
|
||||
|
||||
try:
|
||||
allow_draft75 = _parse_option(
|
||||
_PYOPT_ALLOW_DRAFT75,
|
||||
apache.main_server.get_options().get(_PYOPT_ALLOW_DRAFT75),
|
||||
_PYOPT_ALLOW_DRAFT75_DEFINITION)
|
||||
|
||||
try:
|
||||
handshake.do_handshake(
|
||||
request, _dispatcher, allowDraft75=allow_draft75)
|
||||
except handshake.VersionException, e:
|
||||
request.log_error(
|
||||
'mod_pywebsocket: Handshake failed for version error: %s' % e,
|
||||
apache.APLOG_INFO)
|
||||
request.err_headers_out.add(common.SEC_WEBSOCKET_VERSION_HEADER,
|
||||
e.supported_versions)
|
||||
return apache.HTTP_BAD_REQUEST
|
||||
except handshake.HandshakeException, e:
|
||||
# Handshake for ws/wss failed.
|
||||
# Send http response with error status.
|
||||
request.log_error(
|
||||
'mod_pywebsocket: Handshake failed for error: %s' % e,
|
||||
apache.APLOG_INFO)
|
||||
return e.status
|
||||
|
||||
handshake_is_done = True
|
||||
request._dispatcher = _dispatcher
|
||||
_dispatcher.transfer_data(request)
|
||||
except handshake.AbortedByUserException, e:
|
||||
request.log_error('mod_pywebsocket: Aborted: %s' % e, apache.APLOG_INFO)
|
||||
except Exception, e:
|
||||
# DispatchException can also be thrown if something is wrong in
|
||||
# pywebsocket code. It's caught here, then.
|
||||
|
||||
request.log_error('mod_pywebsocket: Exception occurred: %s\n%s' %
|
||||
(e, util.get_stack_trace()),
|
||||
apache.APLOG_ERR)
|
||||
# Unknown exceptions before handshake mean Apache must handle its
|
||||
# request with another handler.
|
||||
if not handshake_is_done:
|
||||
return apache.DECLINED
|
||||
# Set assbackwards to suppress response header generation by Apache.
|
||||
request.assbackwards = 1
|
||||
return apache.DONE # Return DONE such that no other handlers are invoked.
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,109 +0,0 @@
|
|||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the COPYING file or at
|
||||
# https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
|
||||
from mod_pywebsocket import util
|
||||
|
||||
|
||||
class XHRBenchmarkHandler(object):
|
||||
def __init__(self, headers, rfile, wfile):
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self.headers = headers
|
||||
self.rfile = rfile
|
||||
self.wfile = wfile
|
||||
|
||||
def do_send(self):
|
||||
content_length = int(self.headers.getheader('Content-Length'))
|
||||
|
||||
self._logger.debug('Requested to receive %s bytes', content_length)
|
||||
|
||||
RECEIVE_BLOCK_SIZE = 1024 * 1024
|
||||
|
||||
bytes_to_receive = content_length
|
||||
while bytes_to_receive > 0:
|
||||
bytes_to_receive_in_this_loop = bytes_to_receive
|
||||
if bytes_to_receive_in_this_loop > RECEIVE_BLOCK_SIZE:
|
||||
bytes_to_receive_in_this_loop = RECEIVE_BLOCK_SIZE
|
||||
received_data = self.rfile.read(bytes_to_receive_in_this_loop)
|
||||
if received_data != ('a' * bytes_to_receive_in_this_loop):
|
||||
self._logger.debug('Request body verification failed')
|
||||
return
|
||||
bytes_to_receive -= len(received_data)
|
||||
if bytes_to_receive < 0:
|
||||
self._logger.debug('Received %d more bytes than expected' %
|
||||
(-bytes_to_receive))
|
||||
return
|
||||
|
||||
# Return the number of received bytes back to the client.
|
||||
response_body = '%d' % content_length
|
||||
self.wfile.write(
|
||||
'HTTP/1.1 200 OK\r\n'
|
||||
'Content-Type: text/html\r\n'
|
||||
'Content-Length: %d\r\n'
|
||||
'\r\n%s' % (len(response_body), response_body))
|
||||
self.wfile.flush()
|
||||
|
||||
def do_receive(self):
|
||||
content_length = int(self.headers.getheader('Content-Length'))
|
||||
request_body = self.rfile.read(content_length)
|
||||
|
||||
request_array = request_body.split(' ')
|
||||
if len(request_array) < 2:
|
||||
self._logger.debug('Malformed request body: %r', request_body)
|
||||
return
|
||||
|
||||
# Parse the size parameter.
|
||||
bytes_to_send = request_array[0]
|
||||
try:
|
||||
bytes_to_send = int(bytes_to_send)
|
||||
except ValueError, e:
|
||||
self._logger.debug('Malformed size parameter: %r', bytes_to_send)
|
||||
return
|
||||
self._logger.debug('Requested to send %s bytes', bytes_to_send)
|
||||
|
||||
# Parse the transfer encoding parameter.
|
||||
chunked_mode = False
|
||||
mode_parameter = request_array[1]
|
||||
if mode_parameter == 'chunked':
|
||||
self._logger.debug('Requested chunked transfer encoding')
|
||||
chunked_mode = True
|
||||
elif mode_parameter != 'none':
|
||||
self._logger.debug('Invalid mode parameter: %r', mode_parameter)
|
||||
return
|
||||
|
||||
# Write a header
|
||||
response_header = (
|
||||
'HTTP/1.1 200 OK\r\n'
|
||||
'Content-Type: application/octet-stream\r\n')
|
||||
if chunked_mode:
|
||||
response_header += 'Transfer-Encoding: chunked\r\n\r\n'
|
||||
else:
|
||||
response_header += (
|
||||
'Content-Length: %d\r\n\r\n' % bytes_to_send)
|
||||
self.wfile.write(response_header)
|
||||
self.wfile.flush()
|
||||
|
||||
# Write a body
|
||||
SEND_BLOCK_SIZE = 1024 * 1024
|
||||
|
||||
while bytes_to_send > 0:
|
||||
bytes_to_send_in_this_loop = bytes_to_send
|
||||
if bytes_to_send_in_this_loop > SEND_BLOCK_SIZE:
|
||||
bytes_to_send_in_this_loop = SEND_BLOCK_SIZE
|
||||
|
||||
if chunked_mode:
|
||||
self.wfile.write('%x\r\n' % bytes_to_send_in_this_loop)
|
||||
self.wfile.write('a' * bytes_to_send_in_this_loop)
|
||||
if chunked_mode:
|
||||
self.wfile.write('\r\n')
|
||||
self.wfile.flush()
|
||||
|
||||
bytes_to_send -= bytes_to_send_in_this_loop
|
||||
|
||||
if chunked_mode:
|
||||
self.wfile.write('0\r\n\r\n')
|
||||
self.wfile.flush()
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,30 @@
|
|||
# How to Contribute
|
||||
|
||||
We'd love to accept your patches and contributions to this project. There are
|
||||
just a few small guidelines you need to follow.
|
||||
|
||||
## Contributor License Agreement
|
||||
|
||||
Contributions to this project must be accompanied by a Contributor License
|
||||
Agreement. You (or your employer) retain the copyright to your contribution;
|
||||
this simply gives us permission to use and redistribute your contributions as
|
||||
part of the project. Head over to <https://cla.developers.google.com/> to see
|
||||
your current agreements on file or to sign a new one.
|
||||
|
||||
You generally only need to submit a CLA once, so if you've already submitted one
|
||||
(even if it was for a different project), you probably don't need to do it
|
||||
again.
|
||||
|
||||
## Code reviews
|
||||
|
||||
All submissions, including submissions by project members, require review. We
|
||||
use GitHub pull requests for this purpose. Consult
|
||||
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
|
||||
information on using pull requests.
|
||||
For instructions for contributing code, please read:
|
||||
https://github.com/google/pywebsocket/wiki/CodeReviewInstruction
|
||||
|
||||
## Community Guidelines
|
||||
|
||||
This project follows
|
||||
[Google's Open Source Community Guidelines](https://opensource.google/conduct/).
|
|
@ -1,4 +1,4 @@
|
|||
Copyright 2012, Google Inc.
|
||||
Copyright 2020, Google Inc.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
|
@ -0,0 +1,74 @@
|
|||
This pywebsocket code is mostly unchanged from the source at
|
||||
|
||||
https://github.com/GoogleChromeLabs/pywebsocket3
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
STEPS TO UPDATE MOZILLA TO NEWER PYWEBSOCKET VERSION
|
||||
--------------------------------------------------------------------------------
|
||||
- Get new pywebsocket checkout from googlecode (into, for instance, 'src')
|
||||
|
||||
git clone https://github.com/GoogleChromeLabs/pywebsocket3 pywebsocket-read-only
|
||||
cp -r pywebsocket-read-only/mod_pywebsocket testing/mochitest/pywebsocket3
|
||||
|
||||
- hg add/rm appropriate files, and add/remove them from
|
||||
testing/mochitest/moz.build
|
||||
|
||||
- We need to apply the patch to hybi.py that makes HSTS work: (attached at end
|
||||
of this README)
|
||||
|
||||
- Test and make sure the code works:
|
||||
|
||||
mach mochitest dom/websocket/tests
|
||||
|
||||
- If this doesn't take a look at the pywebsocket server log,
|
||||
$OBJDIR/_tests/testing/mochitest/websock.log
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
PATCH TO hybi.py for HSTS support:
|
||||
|
||||
|
||||
diff --git a/testing/mochitest/pywebsocket3/mod_pywebsocket/handshake/hybi.py b/testing/mochitest/pywebsocket3/mod_pywebsocket/handshake/hybi.py
|
||||
--- a/testing/mochitest/pywebsocket3/mod_pywebsocket/handshake/hybi.py
|
||||
+++ b/testing/mochitest/pywebsocket3/mod_pywebsocket/handshake/hybi.py
|
||||
@@ -273,16 +273,19 @@ class Handshaker(object):
|
||||
status=common.HTTP_STATUS_BAD_REQUEST)
|
||||
raise VersionException('Unsupported version %r for header %s' %
|
||||
(version, common.SEC_WEBSOCKET_VERSION_HEADER),
|
||||
supported_versions=', '.join(
|
||||
map(str, _SUPPORTED_VERSIONS)))
|
||||
|
||||
def _set_protocol(self):
|
||||
self._request.ws_protocol = None
|
||||
+ # MOZILLA
|
||||
+ self._request.sts = None
|
||||
+ # /MOZILLA
|
||||
|
||||
protocol_header = self._request.headers_in.get(
|
||||
common.SEC_WEBSOCKET_PROTOCOL_HEADER)
|
||||
|
||||
if protocol_header is None:
|
||||
self._request.ws_requested_protocols = None
|
||||
return
|
||||
|
||||
@@ -371,16 +374,21 @@ class Handshaker(object):
|
||||
format_header(common.SEC_WEBSOCKET_PROTOCOL_HEADER,
|
||||
self._request.ws_protocol))
|
||||
if (self._request.ws_extensions is not None
|
||||
and len(self._request.ws_extensions) != 0):
|
||||
response.append(
|
||||
format_header(
|
||||
common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
|
||||
common.format_extensions(self._request.ws_extensions)))
|
||||
+ # MOZILLA
|
||||
+ if self._request.sts is not None:
|
||||
+ response.append(format_header("Strict-Transport-Security",
|
||||
+ self._request.sts))
|
||||
+ # /MOZILLA
|
||||
|
||||
# Headers not specific for WebSocket
|
||||
for name, value in self._request.extra_headers:
|
||||
response.append(format_header(name, value))
|
||||
|
||||
response.append(u'\r\n')
|
||||
|
||||
return u''.join(response)
|
|
@ -0,0 +1,36 @@
|
|||
|
||||
# pywebsocket3 #
|
||||
|
||||
The pywebsocket project aims to provide a [WebSocket](https://tools.ietf.org/html/rfc6455) standalone server.
|
||||
|
||||
pywebsocket is intended for **testing** or **experimental** purposes.
|
||||
|
||||
Run this to read the general document:
|
||||
```
|
||||
$ pydoc mod_pywebsocket
|
||||
```
|
||||
|
||||
Please see [Wiki](../../wiki) for more details.
|
||||
|
||||
# INSTALL #
|
||||
|
||||
To install this package to the system, run this:
|
||||
```
|
||||
$ python setup.py build
|
||||
$ sudo python setup.py install
|
||||
```
|
||||
|
||||
To install this package as a normal user, run this instead:
|
||||
|
||||
```
|
||||
$ python setup.py build
|
||||
$ python setup.py install --user
|
||||
```
|
||||
# LAUNCH #
|
||||
|
||||
To use pywebsocket as standalone server, run this to read the document:
|
||||
```
|
||||
$ pydoc mod_pywebsocket.standalone
|
||||
```
|
||||
# Disclaimer #
|
||||
This is not an officially supported Google product
|
|
@ -26,63 +26,23 @@
|
|||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
""" A Standalone WebSocket Server for testing purposes
|
||||
|
||||
|
||||
"""WebSocket extension for Apache HTTP Server.
|
||||
|
||||
mod_pywebsocket is a WebSocket extension for Apache HTTP Server
|
||||
intended for testing or experimental purposes. mod_python is required.
|
||||
|
||||
mod_pywebsocket is an API that provides WebSocket functionalities with
|
||||
a standalone WebSocket server. It is intended for testing or
|
||||
experimental purposes.
|
||||
|
||||
Installation
|
||||
============
|
||||
1. Follow standalone server documentation to start running the
|
||||
standalone server. It can be read by running the following command:
|
||||
|
||||
0. Prepare an Apache HTTP Server for which mod_python is enabled.
|
||||
$ pydoc mod_pywebsocket.standalone
|
||||
|
||||
1. Specify the following Apache HTTP Server directives to suit your
|
||||
configuration.
|
||||
|
||||
If mod_pywebsocket is not in the Python path, specify the following.
|
||||
<websock_lib> is the directory where mod_pywebsocket is installed.
|
||||
|
||||
PythonPath "sys.path+['<websock_lib>']"
|
||||
|
||||
Always specify the following. <websock_handlers> is the directory where
|
||||
user-written WebSocket handlers are placed.
|
||||
|
||||
PythonOption mod_pywebsocket.handler_root <websock_handlers>
|
||||
PythonHeaderParserHandler mod_pywebsocket.headerparserhandler
|
||||
|
||||
To limit the search for WebSocket handlers to a directory <scan_dir>
|
||||
under <websock_handlers>, configure as follows:
|
||||
|
||||
PythonOption mod_pywebsocket.handler_scan <scan_dir>
|
||||
|
||||
<scan_dir> is useful in saving scan time when <websock_handlers>
|
||||
contains many non-WebSocket handler files.
|
||||
|
||||
If you want to allow handlers whose canonical path is not under the root
|
||||
directory (i.e. symbolic link is in root directory but its target is not),
|
||||
configure as follows:
|
||||
|
||||
PythonOption mod_pywebsocket.allow_handlers_outside_root_dir On
|
||||
|
||||
Example snippet of httpd.conf:
|
||||
(mod_pywebsocket is in /websock_lib, WebSocket handlers are in
|
||||
/websock_handlers, port is 80 for ws, 443 for wss.)
|
||||
|
||||
<IfModule python_module>
|
||||
PythonPath "sys.path+['/websock_lib']"
|
||||
PythonOption mod_pywebsocket.handler_root /websock_handlers
|
||||
PythonHeaderParserHandler mod_pywebsocket.headerparserhandler
|
||||
</IfModule>
|
||||
|
||||
2. Tune Apache parameters for serving WebSocket. We'd like to note that at
|
||||
least TimeOut directive from core features and RequestReadTimeout
|
||||
directive from mod_reqtimeout should be modified not to kill connections
|
||||
in only a few seconds of idle time.
|
||||
|
||||
3. Verify installation. You can use example/console.html to poke the server.
|
||||
2. Once the standalone server is launched verify it by accessing
|
||||
http://localhost[:port]/console.html. Include the port number when
|
||||
specified on launch. If everything is working correctly, you
|
||||
will see a simple echo console.
|
||||
|
||||
|
||||
Writing WebSocket handlers
|
||||
|
@ -106,8 +66,8 @@ where:
|
|||
request: mod_python request.
|
||||
|
||||
web_socket_do_extra_handshake is called during the handshake after the
|
||||
headers are successfully parsed and WebSocket properties (ws_location,
|
||||
ws_origin, and ws_resource) are added to request. A handler
|
||||
headers are successfully parsed and WebSocket properties (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
|
||||
|
@ -115,11 +75,10 @@ extra handshake (web_socket_do_extra_handshake):
|
|||
- ws_resource
|
||||
- ws_origin
|
||||
- ws_version
|
||||
- ws_location (HyBi 00 only)
|
||||
- ws_extensions (HyBi 06 and later)
|
||||
- ws_deflate (HyBi 06 and later)
|
||||
- ws_extensions
|
||||
- ws_deflate
|
||||
- ws_protocol
|
||||
- ws_requested_protocols (HyBi 06 and later)
|
||||
- ws_requested_protocols
|
||||
|
||||
The last two are a bit tricky. See the next subsection.
|
||||
|
||||
|
@ -127,21 +86,11 @@ The last two are a bit tricky. See the next subsection.
|
|||
Subprotocol Negotiation
|
||||
-----------------------
|
||||
|
||||
For HyBi 06 and later, ws_protocol is always set to None when
|
||||
ws_protocol is always set to None when
|
||||
web_socket_do_extra_handshake is called. If ws_requested_protocols is not
|
||||
None, you must choose one subprotocol from this list and set it to
|
||||
ws_protocol.
|
||||
|
||||
For HyBi 00, when web_socket_do_extra_handshake is called,
|
||||
ws_protocol is set to the value given by the client in
|
||||
Sec-WebSocket-Protocol header or None if
|
||||
such header was not found in the opening handshake request. Finish extra
|
||||
handshake with ws_protocol untouched to accept the request subprotocol.
|
||||
Then, Sec-WebSocket-Protocol header will be sent to
|
||||
the client in response with the same value as requested. Raise an exception
|
||||
in web_socket_do_extra_handshake to reject the requested subprotocol.
|
||||
|
||||
|
||||
Data Transfer
|
||||
-------------
|
||||
|
||||
|
@ -189,8 +138,8 @@ use in web_socket_passive_closing_handshake.
|
|||
Threading
|
||||
---------
|
||||
|
||||
A WebSocket handler must be thread-safe if the server (Apache or
|
||||
standalone.py) is configured to use threads.
|
||||
A WebSocket handler must be thread-safe. The standalone
|
||||
server uses threads by default.
|
||||
|
||||
|
||||
Configuring WebSocket Extension Processors
|
||||
|
@ -220,5 +169,4 @@ A request object has these extension processing related attributes.
|
|||
extension you want to configure from it, and call its methods.
|
||||
"""
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et tw=72
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright 2011, Google Inc.
|
||||
# Copyright 2020, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
|
@ -26,32 +26,57 @@
|
|||
# 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.
|
||||
"""Stream Exceptions.
|
||||
"""
|
||||
|
||||
# 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._stream_base import BadOperationException
|
||||
from mod_pywebsocket._stream_base import ConnectionTerminatedException
|
||||
from mod_pywebsocket._stream_base import InvalidFrameException
|
||||
from mod_pywebsocket._stream_base import InvalidUTF8Exception
|
||||
from mod_pywebsocket._stream_base import UnsupportedFrameException
|
||||
from mod_pywebsocket._stream_hixie75 import StreamHixie75
|
||||
from mod_pywebsocket._stream_hybi import Frame
|
||||
from mod_pywebsocket._stream_hybi import Stream
|
||||
from mod_pywebsocket._stream_hybi import StreamOptions
|
||||
|
||||
# These methods are intended to be used by WebSocket client developers to have
|
||||
# their implementations receive broken data in tests.
|
||||
from mod_pywebsocket._stream_hybi import create_close_frame
|
||||
from mod_pywebsocket._stream_hybi import create_header
|
||||
from mod_pywebsocket._stream_hybi import create_length_header
|
||||
from mod_pywebsocket._stream_hybi import create_ping_frame
|
||||
from mod_pywebsocket._stream_hybi import create_pong_frame
|
||||
from mod_pywebsocket._stream_hybi import create_binary_frame
|
||||
from mod_pywebsocket._stream_hybi import create_text_frame
|
||||
from mod_pywebsocket._stream_hybi import create_closing_handshake_body
|
||||
# Exceptions
|
||||
class ConnectionTerminatedException(Exception):
|
||||
"""This exception will be raised when a connection is terminated
|
||||
unexpectedly.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidFrameException(ConnectionTerminatedException):
|
||||
"""This exception will be raised when we received an invalid frame we
|
||||
cannot parse.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BadOperationException(Exception):
|
||||
"""This exception will be raised when send_message() is called on
|
||||
server-terminated connection or receive_message() is called on
|
||||
client-terminated connection.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedFrameException(Exception):
|
||||
"""This exception will be raised when we receive a frame with flag, opcode
|
||||
we cannot handle. Handlers can just catch and ignore this exception and
|
||||
call receive_message() again to continue processing the next frame.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidUTF8Exception(Exception):
|
||||
"""This exception will be raised when we receive a text frame which
|
||||
contains invalid UTF-8 strings.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -26,33 +26,16 @@
|
|||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""This file must not depend on any module specific to the WebSocket protocol.
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
from mod_pywebsocket import http_header_util
|
||||
|
||||
|
||||
# Additional log level definitions.
|
||||
LOGLEVEL_FINE = 9
|
||||
|
||||
# Constants indicating WebSocket protocol version.
|
||||
VERSION_HIXIE75 = -1
|
||||
VERSION_HYBI00 = 0
|
||||
VERSION_HYBI01 = 1
|
||||
VERSION_HYBI02 = 2
|
||||
VERSION_HYBI03 = 2
|
||||
VERSION_HYBI04 = 4
|
||||
VERSION_HYBI05 = 5
|
||||
VERSION_HYBI06 = 6
|
||||
VERSION_HYBI07 = 7
|
||||
VERSION_HYBI08 = 8
|
||||
VERSION_HYBI09 = 8
|
||||
VERSION_HYBI10 = 8
|
||||
VERSION_HYBI11 = 8
|
||||
VERSION_HYBI12 = 8
|
||||
VERSION_HYBI13 = 13
|
||||
VERSION_HYBI14 = 13
|
||||
VERSION_HYBI15 = 13
|
||||
|
@ -78,33 +61,24 @@ OPCODE_CLOSE = 0x8
|
|||
OPCODE_PING = 0x9
|
||||
OPCODE_PONG = 0xa
|
||||
|
||||
# UUIDs used by HyBi 04 and later opening handshake and frame masking.
|
||||
WEBSOCKET_ACCEPT_UUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
||||
# UUID for the opening handshake and frame masking.
|
||||
WEBSOCKET_ACCEPT_UUID = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
||||
|
||||
# Opening handshake header names and expected values.
|
||||
UPGRADE_HEADER = 'Upgrade'
|
||||
WEBSOCKET_UPGRADE_TYPE = 'websocket'
|
||||
WEBSOCKET_UPGRADE_TYPE_HIXIE75 = 'WebSocket'
|
||||
CONNECTION_HEADER = 'Connection'
|
||||
UPGRADE_CONNECTION_TYPE = 'Upgrade'
|
||||
HOST_HEADER = 'Host'
|
||||
ORIGIN_HEADER = 'Origin'
|
||||
SEC_WEBSOCKET_ORIGIN_HEADER = 'Sec-WebSocket-Origin'
|
||||
SEC_WEBSOCKET_KEY_HEADER = 'Sec-WebSocket-Key'
|
||||
SEC_WEBSOCKET_ACCEPT_HEADER = 'Sec-WebSocket-Accept'
|
||||
SEC_WEBSOCKET_VERSION_HEADER = 'Sec-WebSocket-Version'
|
||||
SEC_WEBSOCKET_PROTOCOL_HEADER = 'Sec-WebSocket-Protocol'
|
||||
SEC_WEBSOCKET_EXTENSIONS_HEADER = 'Sec-WebSocket-Extensions'
|
||||
SEC_WEBSOCKET_DRAFT_HEADER = 'Sec-WebSocket-Draft'
|
||||
SEC_WEBSOCKET_KEY1_HEADER = 'Sec-WebSocket-Key1'
|
||||
SEC_WEBSOCKET_KEY2_HEADER = 'Sec-WebSocket-Key2'
|
||||
SEC_WEBSOCKET_LOCATION_HEADER = 'Sec-WebSocket-Location'
|
||||
|
||||
# Extensions
|
||||
DEFLATE_FRAME_EXTENSION = 'deflate-frame'
|
||||
PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate'
|
||||
X_WEBKIT_DEFLATE_FRAME_EXTENSION = 'x-webkit-deflate-frame'
|
||||
MUX_EXTENSION = 'mux_DO_NOT_USE'
|
||||
|
||||
# Status codes
|
||||
# Code STATUS_NO_STATUS_RECEIVED, STATUS_ABNORMAL_CLOSURE, and
|
||||
|
@ -151,10 +125,7 @@ def is_control_opcode(opcode):
|
|||
|
||||
|
||||
class ExtensionParameter(object):
|
||||
"""Holds information about an extension which is exchanged on extension
|
||||
negotiation in opening handshake.
|
||||
"""
|
||||
|
||||
"""This is exchanged on extension negotiation in opening handshake."""
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
# TODO(tyoshino): Change the data structure to more efficient one such
|
||||
|
@ -164,30 +135,37 @@ class ExtensionParameter(object):
|
|||
self._parameters = []
|
||||
|
||||
def name(self):
|
||||
"""Return the extension name."""
|
||||
return self._name
|
||||
|
||||
def add_parameter(self, name, value):
|
||||
"""Add a parameter."""
|
||||
self._parameters.append((name, value))
|
||||
|
||||
def get_parameters(self):
|
||||
"""Return the parameters."""
|
||||
return self._parameters
|
||||
|
||||
def get_parameter_names(self):
|
||||
"""Return the names of the parameters."""
|
||||
return [name for name, unused_value in self._parameters]
|
||||
|
||||
def has_parameter(self, name):
|
||||
"""Test if a parameter exists."""
|
||||
for param_name, param_value in self._parameters:
|
||||
if param_name == name:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_parameter_value(self, name):
|
||||
"""Get the value of a specific parameter."""
|
||||
for param_name, param_value in self._parameters:
|
||||
if param_name == name:
|
||||
return param_value
|
||||
|
||||
|
||||
class ExtensionParsingException(Exception):
|
||||
"""Exception to handle errors in extension parsing."""
|
||||
def __init__(self, name):
|
||||
super(ExtensionParsingException, self).__init__(name)
|
||||
|
||||
|
@ -233,21 +211,19 @@ def _parse_extension(state):
|
|||
|
||||
try:
|
||||
_parse_extension_param(state, extension)
|
||||
except ExtensionParsingException, e:
|
||||
except ExtensionParsingException as e:
|
||||
raise ExtensionParsingException(
|
||||
'Failed to parse parameter for %r (%r)' %
|
||||
(extension_token, e))
|
||||
'Failed to parse parameter for %r (%r)' % (extension_token, e))
|
||||
|
||||
return extension
|
||||
|
||||
|
||||
def parse_extensions(data):
|
||||
"""Parses Sec-WebSocket-Extensions header value returns a list of
|
||||
ExtensionParameter objects.
|
||||
"""Parse Sec-WebSocket-Extensions header value.
|
||||
|
||||
Returns a list of ExtensionParameter objects.
|
||||
Leading LWSes must be trimmed.
|
||||
"""
|
||||
|
||||
state = http_header_util.ParsingState(data)
|
||||
|
||||
extension_list = []
|
||||
|
@ -264,21 +240,18 @@ def parse_extensions(data):
|
|||
if not http_header_util.consume_string(state, ','):
|
||||
raise ExtensionParsingException(
|
||||
'Failed to parse Sec-WebSocket-Extensions header: '
|
||||
'Expected a comma but found %r' %
|
||||
http_header_util.peek(state))
|
||||
'Expected a comma but found %r' % http_header_util.peek(state))
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
if len(extension_list) == 0:
|
||||
raise ExtensionParsingException(
|
||||
'No valid extension entry found')
|
||||
raise ExtensionParsingException('No valid extension entry found')
|
||||
|
||||
return extension_list
|
||||
|
||||
|
||||
def format_extension(extension):
|
||||
"""Formats an ExtensionParameter object."""
|
||||
|
||||
"""Format an ExtensionParameter object."""
|
||||
formatted_params = [extension.name()]
|
||||
for param_name, param_value in extension.get_parameters():
|
||||
if param_value is None:
|
||||
|
@ -290,8 +263,7 @@ def format_extension(extension):
|
|||
|
||||
|
||||
def format_extensions(extension_list):
|
||||
"""Formats a list of ExtensionParameter objects."""
|
||||
|
||||
"""Format a list of ExtensionParameter objects."""
|
||||
formatted_extension_list = []
|
||||
for extension in extension_list:
|
||||
formatted_extension_list.append(format_extension(extension))
|
|
@ -26,24 +26,21 @@
|
|||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Dispatch WebSocket request.
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket import handshake
|
||||
from mod_pywebsocket import msgutil
|
||||
from mod_pywebsocket import mux
|
||||
from mod_pywebsocket import stream
|
||||
from mod_pywebsocket import util
|
||||
|
||||
|
||||
_SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$')
|
||||
_SOURCE_SUFFIX = '_wsh.py'
|
||||
_DO_EXTRA_HANDSHAKE_HANDLER_NAME = 'web_socket_do_extra_handshake'
|
||||
|
@ -54,7 +51,6 @@ _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME = (
|
|||
|
||||
class DispatchException(Exception):
|
||||
"""Exception in dispatching WebSocket request."""
|
||||
|
||||
def __init__(self, name, status=common.HTTP_STATUS_NOT_FOUND):
|
||||
super(DispatchException, self).__init__(name)
|
||||
self.status = status
|
||||
|
@ -125,7 +121,6 @@ def _enumerate_handler_file_paths(directory):
|
|||
|
||||
class _HandlerSuite(object):
|
||||
"""A handler suite holder class."""
|
||||
|
||||
def __init__(self, do_extra_handshake, transfer_data,
|
||||
passive_closing_handshake):
|
||||
self.do_extra_handshake = do_extra_handshake
|
||||
|
@ -143,10 +138,13 @@ def _source_handler_file(handler_definition):
|
|||
|
||||
global_dic = {}
|
||||
try:
|
||||
exec handler_definition in global_dic
|
||||
# This statement is gramatically different in python 2 and 3.
|
||||
# Hence, yapf will complain about this. To overcome this, we disable
|
||||
# yapf for this line.
|
||||
exec(handler_definition, global_dic) # yapf: disable
|
||||
except Exception:
|
||||
raise DispatchException('Error in sourcing handler:' +
|
||||
util.get_stack_trace())
|
||||
traceback.format_exc())
|
||||
passive_closing_handshake_handler = None
|
||||
try:
|
||||
passive_closing_handshake_handler = _extract_handler(
|
||||
|
@ -178,10 +176,10 @@ class Dispatcher(object):
|
|||
|
||||
This class maintains a map from resource name to handlers.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, root_dir, scan_dir=None,
|
||||
allow_handlers_outside_root_dir=True):
|
||||
def __init__(self,
|
||||
root_dir,
|
||||
scan_dir=None,
|
||||
allow_handlers_outside_root_dir=True):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
|
@ -207,11 +205,11 @@ class Dispatcher(object):
|
|||
os.path.realpath(root_dir)):
|
||||
raise DispatchException('scan_dir:%s must be a directory under '
|
||||
'root_dir:%s.' % (scan_dir, root_dir))
|
||||
self._source_handler_files_in_dir(
|
||||
root_dir, scan_dir, allow_handlers_outside_root_dir)
|
||||
self._source_handler_files_in_dir(root_dir, scan_dir,
|
||||
allow_handlers_outside_root_dir)
|
||||
|
||||
def add_resource_path_alias(self,
|
||||
alias_resource_path, existing_resource_path):
|
||||
def add_resource_path_alias(self, alias_resource_path,
|
||||
existing_resource_path):
|
||||
"""Add resource path alias.
|
||||
|
||||
Once added, request to alias_resource_path would be handled by
|
||||
|
@ -254,17 +252,15 @@ class Dispatcher(object):
|
|||
do_extra_handshake_ = handler_suite.do_extra_handshake
|
||||
try:
|
||||
do_extra_handshake_(request)
|
||||
except handshake.AbortedByUserException, e:
|
||||
except handshake.AbortedByUserException as e:
|
||||
# Re-raise to tell the caller of this function to finish this
|
||||
# connection without sending any error.
|
||||
self._logger.debug('%s', util.get_stack_trace())
|
||||
self._logger.debug('%s', traceback.format_exc())
|
||||
raise
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
util.prepend_message_to_exception(
|
||||
'%s raised exception for %s: ' % (
|
||||
_DO_EXTRA_HANDSHAKE_HANDLER_NAME,
|
||||
request.ws_resource),
|
||||
e)
|
||||
'%s raised exception for %s: ' %
|
||||
(_DO_EXTRA_HANDSHAKE_HANDLER_NAME, request.ws_resource), e)
|
||||
raise handshake.HandshakeException(e, common.HTTP_STATUS_FORBIDDEN)
|
||||
|
||||
def transfer_data(self, request):
|
||||
|
@ -283,47 +279,43 @@ class Dispatcher(object):
|
|||
|
||||
# TODO(tyoshino): Terminate underlying TCP connection if possible.
|
||||
try:
|
||||
if mux.use_mux(request):
|
||||
mux.start(request, self)
|
||||
else:
|
||||
handler_suite = self.get_handler_suite(request.ws_resource)
|
||||
if handler_suite is None:
|
||||
raise DispatchException('No handler for: %r' %
|
||||
request.ws_resource)
|
||||
transfer_data_ = handler_suite.transfer_data
|
||||
transfer_data_(request)
|
||||
handler_suite = self.get_handler_suite(request.ws_resource)
|
||||
if handler_suite is None:
|
||||
raise DispatchException('No handler for: %r' %
|
||||
request.ws_resource)
|
||||
transfer_data_ = handler_suite.transfer_data
|
||||
transfer_data_(request)
|
||||
|
||||
if not request.server_terminated:
|
||||
request.ws_stream.close_connection()
|
||||
# Catch non-critical exceptions the handler didn't handle.
|
||||
except handshake.AbortedByUserException, e:
|
||||
self._logger.debug('%s', util.get_stack_trace())
|
||||
except handshake.AbortedByUserException as e:
|
||||
self._logger.debug('%s', traceback.format_exc())
|
||||
raise
|
||||
except msgutil.BadOperationException, e:
|
||||
except msgutil.BadOperationException as e:
|
||||
self._logger.debug('%s', e)
|
||||
request.ws_stream.close_connection(
|
||||
common.STATUS_INTERNAL_ENDPOINT_ERROR)
|
||||
except msgutil.InvalidFrameException, e:
|
||||
except msgutil.InvalidFrameException as e:
|
||||
# InvalidFrameException must be caught before
|
||||
# ConnectionTerminatedException that catches InvalidFrameException.
|
||||
self._logger.debug('%s', e)
|
||||
request.ws_stream.close_connection(common.STATUS_PROTOCOL_ERROR)
|
||||
except msgutil.UnsupportedFrameException, e:
|
||||
except msgutil.UnsupportedFrameException as e:
|
||||
self._logger.debug('%s', e)
|
||||
request.ws_stream.close_connection(common.STATUS_UNSUPPORTED_DATA)
|
||||
except stream.InvalidUTF8Exception, e:
|
||||
except stream.InvalidUTF8Exception as e:
|
||||
self._logger.debug('%s', e)
|
||||
request.ws_stream.close_connection(
|
||||
common.STATUS_INVALID_FRAME_PAYLOAD_DATA)
|
||||
except msgutil.ConnectionTerminatedException, e:
|
||||
except msgutil.ConnectionTerminatedException as e:
|
||||
self._logger.debug('%s', e)
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
# Any other exceptions are forwarded to the caller of this
|
||||
# function.
|
||||
util.prepend_message_to_exception(
|
||||
'%s raised exception for %s: ' % (
|
||||
_TRANSFER_DATA_HANDLER_NAME, request.ws_resource),
|
||||
e)
|
||||
'%s raised exception for %s: ' %
|
||||
(_TRANSFER_DATA_HANDLER_NAME, request.ws_resource), e)
|
||||
raise
|
||||
|
||||
def passive_closing_handshake(self, request):
|
||||
|
@ -348,13 +340,13 @@ class Dispatcher(object):
|
|||
resource = resource.split('?', 1)[0]
|
||||
handler_suite = self._handler_suite_map.get(resource)
|
||||
if handler_suite and fragment:
|
||||
raise DispatchException('Fragment identifiers MUST NOT be used on '
|
||||
'WebSocket URIs',
|
||||
common.HTTP_STATUS_BAD_REQUEST)
|
||||
raise DispatchException(
|
||||
'Fragment identifiers MUST NOT be used on WebSocket URIs',
|
||||
common.HTTP_STATUS_BAD_REQUEST)
|
||||
return handler_suite
|
||||
|
||||
def _source_handler_files_in_dir(
|
||||
self, root_dir, scan_dir, allow_handlers_outside_root_dir):
|
||||
def _source_handler_files_in_dir(self, root_dir, scan_dir,
|
||||
allow_handlers_outside_root_dir):
|
||||
"""Source all the handler source files in the scan_dir directory.
|
||||
|
||||
The resource path is determined relative to root_dir.
|
||||
|
@ -374,18 +366,18 @@ class Dispatcher(object):
|
|||
if (not allow_handlers_outside_root_dir and
|
||||
(not os.path.realpath(path).startswith(root_realpath))):
|
||||
self._logger.debug(
|
||||
'Canonical path of %s is not under root directory' %
|
||||
path)
|
||||
'Canonical path of %s is not under root directory' % path)
|
||||
continue
|
||||
try:
|
||||
handler_suite = _source_handler_file(open(path).read())
|
||||
except DispatchException, e:
|
||||
with open(path) as handler_file:
|
||||
handler_suite = _source_handler_file(handler_file.read())
|
||||
except DispatchException as e:
|
||||
self._source_warnings.append('%s: %s' % (path, e))
|
||||
continue
|
||||
resource = convert(path)
|
||||
if resource is None:
|
||||
self._logger.debug(
|
||||
'Path to resource conversion on %s failed' % path)
|
||||
self._logger.debug('Path to resource conversion on %s failed' %
|
||||
path)
|
||||
else:
|
||||
self._handler_suite_map[convert(path)] = handler_suite
|
||||
|
|
@ -27,19 +27,16 @@
|
|||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket import util
|
||||
from mod_pywebsocket.http_header_util import quote_if_necessary
|
||||
|
||||
|
||||
# The list of available server side extension processor classes.
|
||||
_available_processors = {}
|
||||
_compression_extension_names = []
|
||||
|
||||
|
||||
class ExtensionProcessorInterface(object):
|
||||
|
||||
def __init__(self, request):
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
|
@ -82,26 +79,26 @@ class ExtensionProcessorInterface(object):
|
|||
self._setup_stream_options_internal(stream_options)
|
||||
|
||||
|
||||
def _log_outgoing_compression_ratio(
|
||||
logger, original_bytes, filtered_bytes, average_ratio):
|
||||
def _log_outgoing_compression_ratio(logger, original_bytes, filtered_bytes,
|
||||
average_ratio):
|
||||
# Print inf when ratio is not available.
|
||||
ratio = float('inf')
|
||||
if original_bytes != 0:
|
||||
ratio = float(filtered_bytes) / original_bytes
|
||||
|
||||
logger.debug('Outgoing compression ratio: %f (average: %f)' %
|
||||
(ratio, average_ratio))
|
||||
(ratio, average_ratio))
|
||||
|
||||
|
||||
def _log_incoming_compression_ratio(
|
||||
logger, received_bytes, filtered_bytes, average_ratio):
|
||||
def _log_incoming_compression_ratio(logger, received_bytes, filtered_bytes,
|
||||
average_ratio):
|
||||
# Print inf when ratio is not available.
|
||||
ratio = float('inf')
|
||||
if filtered_bytes != 0:
|
||||
ratio = float(received_bytes) / filtered_bytes
|
||||
|
||||
logger.debug('Incoming compression ratio: %f (average: %f)' %
|
||||
(ratio, average_ratio))
|
||||
(ratio, average_ratio))
|
||||
|
||||
|
||||
def _parse_window_bits(bits):
|
||||
|
@ -126,7 +123,6 @@ class _AverageRatioCalculator(object):
|
|||
"""Stores total bytes of original and result data, and calculates average
|
||||
result / original ratio.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._total_original_bytes = 0
|
||||
self._total_result_bytes = 0
|
||||
|
@ -145,188 +141,6 @@ class _AverageRatioCalculator(object):
|
|||
return float('inf')
|
||||
|
||||
|
||||
class DeflateFrameExtensionProcessor(ExtensionProcessorInterface):
|
||||
"""deflate-frame extension processor.
|
||||
|
||||
Specification:
|
||||
http://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate
|
||||
"""
|
||||
|
||||
_WINDOW_BITS_PARAM = 'max_window_bits'
|
||||
_NO_CONTEXT_TAKEOVER_PARAM = 'no_context_takeover'
|
||||
|
||||
def __init__(self, request):
|
||||
ExtensionProcessorInterface.__init__(self, request)
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._response_window_bits = None
|
||||
self._response_no_context_takeover = False
|
||||
self._bfinal = False
|
||||
|
||||
# Calculates
|
||||
# (Total outgoing bytes supplied to this filter) /
|
||||
# (Total bytes sent to the network after applying this filter)
|
||||
self._outgoing_average_ratio_calculator = _AverageRatioCalculator()
|
||||
|
||||
# Calculates
|
||||
# (Total bytes received from the network) /
|
||||
# (Total incoming bytes obtained after applying this filter)
|
||||
self._incoming_average_ratio_calculator = _AverageRatioCalculator()
|
||||
|
||||
def name(self):
|
||||
return common.DEFLATE_FRAME_EXTENSION
|
||||
|
||||
def _get_extension_response_internal(self):
|
||||
# Any unknown parameter will be just ignored.
|
||||
|
||||
window_bits = None
|
||||
if self._request.has_parameter(self._WINDOW_BITS_PARAM):
|
||||
window_bits = self._request.get_parameter_value(
|
||||
self._WINDOW_BITS_PARAM)
|
||||
try:
|
||||
window_bits = _parse_window_bits(window_bits)
|
||||
except ValueError, e:
|
||||
return None
|
||||
|
||||
no_context_takeover = self._request.has_parameter(
|
||||
self._NO_CONTEXT_TAKEOVER_PARAM)
|
||||
if (no_context_takeover and
|
||||
self._request.get_parameter_value(
|
||||
self._NO_CONTEXT_TAKEOVER_PARAM) is not None):
|
||||
return None
|
||||
|
||||
self._rfc1979_deflater = util._RFC1979Deflater(
|
||||
window_bits, no_context_takeover)
|
||||
|
||||
self._rfc1979_inflater = util._RFC1979Inflater()
|
||||
|
||||
self._compress_outgoing = True
|
||||
|
||||
response = common.ExtensionParameter(self._request.name())
|
||||
|
||||
if self._response_window_bits is not None:
|
||||
response.add_parameter(
|
||||
self._WINDOW_BITS_PARAM, str(self._response_window_bits))
|
||||
if self._response_no_context_takeover:
|
||||
response.add_parameter(
|
||||
self._NO_CONTEXT_TAKEOVER_PARAM, None)
|
||||
|
||||
self._logger.debug(
|
||||
'Enable %s extension ('
|
||||
'request: window_bits=%s; no_context_takeover=%r, '
|
||||
'response: window_wbits=%s; no_context_takeover=%r)' %
|
||||
(self._request.name(),
|
||||
window_bits,
|
||||
no_context_takeover,
|
||||
self._response_window_bits,
|
||||
self._response_no_context_takeover))
|
||||
|
||||
return response
|
||||
|
||||
def _setup_stream_options_internal(self, stream_options):
|
||||
|
||||
class _OutgoingFilter(object):
|
||||
|
||||
def __init__(self, parent):
|
||||
self._parent = parent
|
||||
|
||||
def filter(self, frame):
|
||||
self._parent._outgoing_filter(frame)
|
||||
|
||||
class _IncomingFilter(object):
|
||||
|
||||
def __init__(self, parent):
|
||||
self._parent = parent
|
||||
|
||||
def filter(self, frame):
|
||||
self._parent._incoming_filter(frame)
|
||||
|
||||
stream_options.outgoing_frame_filters.append(
|
||||
_OutgoingFilter(self))
|
||||
stream_options.incoming_frame_filters.insert(
|
||||
0, _IncomingFilter(self))
|
||||
|
||||
def set_response_window_bits(self, value):
|
||||
self._response_window_bits = value
|
||||
|
||||
def set_response_no_context_takeover(self, value):
|
||||
self._response_no_context_takeover = value
|
||||
|
||||
def set_bfinal(self, value):
|
||||
self._bfinal = value
|
||||
|
||||
def enable_outgoing_compression(self):
|
||||
self._compress_outgoing = True
|
||||
|
||||
def disable_outgoing_compression(self):
|
||||
self._compress_outgoing = False
|
||||
|
||||
def _outgoing_filter(self, frame):
|
||||
"""Transform outgoing frames. This method is called only by
|
||||
an _OutgoingFilter instance.
|
||||
"""
|
||||
|
||||
original_payload_size = len(frame.payload)
|
||||
self._outgoing_average_ratio_calculator.add_original_bytes(
|
||||
original_payload_size)
|
||||
|
||||
if (not self._compress_outgoing or
|
||||
common.is_control_opcode(frame.opcode)):
|
||||
self._outgoing_average_ratio_calculator.add_result_bytes(
|
||||
original_payload_size)
|
||||
return
|
||||
|
||||
frame.payload = self._rfc1979_deflater.filter(
|
||||
frame.payload, bfinal=self._bfinal)
|
||||
frame.rsv1 = 1
|
||||
|
||||
filtered_payload_size = len(frame.payload)
|
||||
self._outgoing_average_ratio_calculator.add_result_bytes(
|
||||
filtered_payload_size)
|
||||
|
||||
_log_outgoing_compression_ratio(
|
||||
self._logger,
|
||||
original_payload_size,
|
||||
filtered_payload_size,
|
||||
self._outgoing_average_ratio_calculator.get_average_ratio())
|
||||
|
||||
def _incoming_filter(self, frame):
|
||||
"""Transform incoming frames. This method is called only by
|
||||
an _IncomingFilter instance.
|
||||
"""
|
||||
|
||||
received_payload_size = len(frame.payload)
|
||||
self._incoming_average_ratio_calculator.add_result_bytes(
|
||||
received_payload_size)
|
||||
|
||||
if frame.rsv1 != 1 or common.is_control_opcode(frame.opcode):
|
||||
self._incoming_average_ratio_calculator.add_original_bytes(
|
||||
received_payload_size)
|
||||
return
|
||||
|
||||
frame.payload = self._rfc1979_inflater.filter(frame.payload)
|
||||
frame.rsv1 = 0
|
||||
|
||||
filtered_payload_size = len(frame.payload)
|
||||
self._incoming_average_ratio_calculator.add_original_bytes(
|
||||
filtered_payload_size)
|
||||
|
||||
_log_incoming_compression_ratio(
|
||||
self._logger,
|
||||
received_payload_size,
|
||||
filtered_payload_size,
|
||||
self._incoming_average_ratio_calculator.get_average_ratio())
|
||||
|
||||
|
||||
_available_processors[common.DEFLATE_FRAME_EXTENSION] = (
|
||||
DeflateFrameExtensionProcessor)
|
||||
_compression_extension_names.append(common.DEFLATE_FRAME_EXTENSION)
|
||||
|
||||
_available_processors[common.X_WEBKIT_DEFLATE_FRAME_EXTENSION] = (
|
||||
DeflateFrameExtensionProcessor)
|
||||
_compression_extension_names.append(common.X_WEBKIT_DEFLATE_FRAME_EXTENSION)
|
||||
|
||||
|
||||
class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface):
|
||||
"""permessage-deflate extension processor.
|
||||
|
||||
|
@ -339,15 +153,8 @@ class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface):
|
|||
_CLIENT_MAX_WINDOW_BITS_PARAM = 'client_max_window_bits'
|
||||
_CLIENT_NO_CONTEXT_TAKEOVER_PARAM = 'client_no_context_takeover'
|
||||
|
||||
def __init__(self, request, draft08=True):
|
||||
"""Construct PerMessageDeflateExtensionProcessor
|
||||
|
||||
Args:
|
||||
draft08: Follow the constraints on the parameters that were not
|
||||
specified for permessage-compress but are specified for
|
||||
permessage-deflate as on
|
||||
draft-ietf-hybi-permessage-compression-08.
|
||||
"""
|
||||
def __init__(self, request):
|
||||
"""Construct PerMessageDeflateExtensionProcessor."""
|
||||
|
||||
ExtensionProcessorInterface.__init__(self, request)
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
@ -355,42 +162,36 @@ class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface):
|
|||
self._preferred_client_max_window_bits = None
|
||||
self._client_no_context_takeover = False
|
||||
|
||||
self._draft08 = draft08
|
||||
|
||||
def name(self):
|
||||
# This method returns "deflate" (not "permessage-deflate") for
|
||||
# compatibility.
|
||||
return 'deflate'
|
||||
|
||||
def _get_extension_response_internal(self):
|
||||
if self._draft08:
|
||||
for name in self._request.get_parameter_names():
|
||||
if name not in [self._SERVER_MAX_WINDOW_BITS_PARAM,
|
||||
self._SERVER_NO_CONTEXT_TAKEOVER_PARAM,
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM]:
|
||||
self._logger.debug('Unknown parameter: %r', name)
|
||||
return None
|
||||
else:
|
||||
# Any unknown parameter will be just ignored.
|
||||
pass
|
||||
for name in self._request.get_parameter_names():
|
||||
if name not in [
|
||||
self._SERVER_MAX_WINDOW_BITS_PARAM,
|
||||
self._SERVER_NO_CONTEXT_TAKEOVER_PARAM,
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM
|
||||
]:
|
||||
self._logger.debug('Unknown parameter: %r', name)
|
||||
return None
|
||||
|
||||
server_max_window_bits = None
|
||||
if self._request.has_parameter(self._SERVER_MAX_WINDOW_BITS_PARAM):
|
||||
server_max_window_bits = self._request.get_parameter_value(
|
||||
self._SERVER_MAX_WINDOW_BITS_PARAM)
|
||||
self._SERVER_MAX_WINDOW_BITS_PARAM)
|
||||
try:
|
||||
server_max_window_bits = _parse_window_bits(
|
||||
server_max_window_bits)
|
||||
except ValueError, e:
|
||||
except ValueError as e:
|
||||
self._logger.debug('Bad %s parameter: %r',
|
||||
self._SERVER_MAX_WINDOW_BITS_PARAM,
|
||||
e)
|
||||
self._SERVER_MAX_WINDOW_BITS_PARAM, e)
|
||||
return None
|
||||
|
||||
server_no_context_takeover = self._request.has_parameter(
|
||||
self._SERVER_NO_CONTEXT_TAKEOVER_PARAM)
|
||||
if (server_no_context_takeover and
|
||||
self._request.get_parameter_value(
|
||||
if (server_no_context_takeover and self._request.get_parameter_value(
|
||||
self._SERVER_NO_CONTEXT_TAKEOVER_PARAM) is not None):
|
||||
self._logger.debug('%s parameter must not have a value: %r',
|
||||
self._SERVER_NO_CONTEXT_TAKEOVER_PARAM,
|
||||
|
@ -401,14 +202,14 @@ class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface):
|
|||
# accept client_max_window_bits from a server or not.
|
||||
client_client_max_window_bits = self._request.has_parameter(
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM)
|
||||
if (self._draft08 and
|
||||
client_client_max_window_bits and
|
||||
self._request.get_parameter_value(
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM) is not None):
|
||||
self._logger.debug('%s parameter must not have a value in a '
|
||||
'client\'s opening handshake: %r',
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM,
|
||||
client_client_max_window_bits)
|
||||
if (client_client_max_window_bits
|
||||
and self._request.get_parameter_value(
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM) is not None):
|
||||
self._logger.debug(
|
||||
'%s parameter must not have a value in a '
|
||||
'client\'s opening handshake: %r',
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM,
|
||||
client_client_max_window_bits)
|
||||
return None
|
||||
|
||||
self._rfc1979_deflater = util._RFC1979Deflater(
|
||||
|
@ -419,47 +220,44 @@ class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface):
|
|||
# sent to the client.
|
||||
self._rfc1979_inflater = util._RFC1979Inflater()
|
||||
|
||||
self._framer = _PerMessageDeflateFramer(
|
||||
server_max_window_bits, server_no_context_takeover)
|
||||
self._framer = _PerMessageDeflateFramer(server_max_window_bits,
|
||||
server_no_context_takeover)
|
||||
self._framer.set_bfinal(False)
|
||||
self._framer.set_compress_outgoing_enabled(True)
|
||||
|
||||
response = common.ExtensionParameter(self._request.name())
|
||||
|
||||
if server_max_window_bits is not None:
|
||||
response.add_parameter(
|
||||
self._SERVER_MAX_WINDOW_BITS_PARAM,
|
||||
str(server_max_window_bits))
|
||||
response.add_parameter(self._SERVER_MAX_WINDOW_BITS_PARAM,
|
||||
str(server_max_window_bits))
|
||||
|
||||
if server_no_context_takeover:
|
||||
response.add_parameter(
|
||||
self._SERVER_NO_CONTEXT_TAKEOVER_PARAM, None)
|
||||
response.add_parameter(self._SERVER_NO_CONTEXT_TAKEOVER_PARAM,
|
||||
None)
|
||||
|
||||
if self._preferred_client_max_window_bits is not None:
|
||||
if self._draft08 and not client_client_max_window_bits:
|
||||
self._logger.debug('Processor is configured to use %s but '
|
||||
'the client cannot accept it',
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM)
|
||||
if not client_client_max_window_bits:
|
||||
self._logger.debug(
|
||||
'Processor is configured to use %s but '
|
||||
'the client cannot accept it',
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM)
|
||||
return None
|
||||
response.add_parameter(
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM,
|
||||
str(self._preferred_client_max_window_bits))
|
||||
response.add_parameter(self._CLIENT_MAX_WINDOW_BITS_PARAM,
|
||||
str(self._preferred_client_max_window_bits))
|
||||
|
||||
if self._client_no_context_takeover:
|
||||
response.add_parameter(
|
||||
self._CLIENT_NO_CONTEXT_TAKEOVER_PARAM, None)
|
||||
response.add_parameter(self._CLIENT_NO_CONTEXT_TAKEOVER_PARAM,
|
||||
None)
|
||||
|
||||
self._logger.debug(
|
||||
'Enable %s extension ('
|
||||
'request: server_max_window_bits=%s; '
|
||||
'server_no_context_takeover=%r, '
|
||||
'response: client_max_window_bits=%s; '
|
||||
'client_no_context_takeover=%r)' %
|
||||
(self._request.name(),
|
||||
server_max_window_bits,
|
||||
server_no_context_takeover,
|
||||
self._preferred_client_max_window_bits,
|
||||
self._client_no_context_takeover))
|
||||
self._logger.debug('Enable %s extension ('
|
||||
'request: server_max_window_bits=%s; '
|
||||
'server_no_context_takeover=%r, '
|
||||
'response: client_max_window_bits=%s; '
|
||||
'client_no_context_takeover=%r)' %
|
||||
(self._request.name(), server_max_window_bits,
|
||||
server_no_context_takeover,
|
||||
self._preferred_client_max_window_bits,
|
||||
self._client_no_context_takeover))
|
||||
|
||||
return response
|
||||
|
||||
|
@ -505,7 +303,6 @@ class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface):
|
|||
|
||||
class _PerMessageDeflateFramer(object):
|
||||
"""A framer for extensions with per-message DEFLATE feature."""
|
||||
|
||||
def __init__(self, deflate_max_window_bits, deflate_no_context_takeover):
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
|
@ -543,19 +340,17 @@ class _PerMessageDeflateFramer(object):
|
|||
|
||||
received_payload_size = len(message)
|
||||
self._incoming_average_ratio_calculator.add_result_bytes(
|
||||
received_payload_size)
|
||||
received_payload_size)
|
||||
|
||||
message = self._rfc1979_inflater.filter(message)
|
||||
|
||||
filtered_payload_size = len(message)
|
||||
self._incoming_average_ratio_calculator.add_original_bytes(
|
||||
filtered_payload_size)
|
||||
filtered_payload_size)
|
||||
|
||||
_log_incoming_compression_ratio(
|
||||
self._logger,
|
||||
received_payload_size,
|
||||
filtered_payload_size,
|
||||
self._incoming_average_ratio_calculator.get_average_ratio())
|
||||
self._logger, received_payload_size, filtered_payload_size,
|
||||
self._incoming_average_ratio_calculator.get_average_ratio())
|
||||
|
||||
return message
|
||||
|
||||
|
@ -570,18 +365,17 @@ class _PerMessageDeflateFramer(object):
|
|||
self._outgoing_average_ratio_calculator.add_original_bytes(
|
||||
original_payload_size)
|
||||
|
||||
message = self._rfc1979_deflater.filter(
|
||||
message, end=end, bfinal=self._bfinal)
|
||||
message = self._rfc1979_deflater.filter(message,
|
||||
end=end,
|
||||
bfinal=self._bfinal)
|
||||
|
||||
filtered_payload_size = len(message)
|
||||
self._outgoing_average_ratio_calculator.add_result_bytes(
|
||||
filtered_payload_size)
|
||||
|
||||
_log_outgoing_compression_ratio(
|
||||
self._logger,
|
||||
original_payload_size,
|
||||
filtered_payload_size,
|
||||
self._outgoing_average_ratio_calculator.get_average_ratio())
|
||||
self._logger, original_payload_size, filtered_payload_size,
|
||||
self._outgoing_average_ratio_calculator.get_average_ratio())
|
||||
|
||||
if not self._compress_ongoing:
|
||||
self._outgoing_frame_filter.set_compression_bit()
|
||||
|
@ -594,17 +388,14 @@ class _PerMessageDeflateFramer(object):
|
|||
frame.rsv1 = 0
|
||||
|
||||
def _process_outgoing_frame(self, frame, compression_bit):
|
||||
if (not compression_bit or
|
||||
common.is_control_opcode(frame.opcode)):
|
||||
if (not compression_bit or common.is_control_opcode(frame.opcode)):
|
||||
return
|
||||
|
||||
frame.rsv1 = 1
|
||||
|
||||
def setup_stream_options(self, stream_options):
|
||||
"""Creates filters and sets them to the StreamOptions."""
|
||||
|
||||
class _OutgoingMessageFilter(object):
|
||||
|
||||
def __init__(self, parent):
|
||||
self._parent = parent
|
||||
|
||||
|
@ -613,7 +404,6 @@ class _PerMessageDeflateFramer(object):
|
|||
message, end, binary)
|
||||
|
||||
class _IncomingMessageFilter(object):
|
||||
|
||||
def __init__(self, parent):
|
||||
self._parent = parent
|
||||
self._decompress_next_message = False
|
||||
|
@ -635,7 +425,6 @@ class _PerMessageDeflateFramer(object):
|
|||
self._incoming_message_filter)
|
||||
|
||||
class _OutgoingFrameFilter(object):
|
||||
|
||||
def __init__(self, parent):
|
||||
self._parent = parent
|
||||
self._set_compression_bit = False
|
||||
|
@ -644,12 +433,11 @@ class _PerMessageDeflateFramer(object):
|
|||
self._set_compression_bit = True
|
||||
|
||||
def filter(self, frame):
|
||||
self._parent._process_outgoing_frame(
|
||||
frame, self._set_compression_bit)
|
||||
self._parent._process_outgoing_frame(frame,
|
||||
self._set_compression_bit)
|
||||
self._set_compression_bit = False
|
||||
|
||||
class _IncomingFrameFilter(object):
|
||||
|
||||
def __init__(self, parent):
|
||||
self._parent = parent
|
||||
|
||||
|
@ -667,82 +455,7 @@ class _PerMessageDeflateFramer(object):
|
|||
|
||||
|
||||
_available_processors[common.PERMESSAGE_DEFLATE_EXTENSION] = (
|
||||
PerMessageDeflateExtensionProcessor)
|
||||
# TODO(tyoshino): Reorganize class names.
|
||||
_compression_extension_names.append('deflate')
|
||||
|
||||
|
||||
class MuxExtensionProcessor(ExtensionProcessorInterface):
|
||||
"""WebSocket multiplexing extension processor."""
|
||||
|
||||
_QUOTA_PARAM = 'quota'
|
||||
|
||||
def __init__(self, request):
|
||||
ExtensionProcessorInterface.__init__(self, request)
|
||||
self._quota = 0
|
||||
self._extensions = []
|
||||
|
||||
def name(self):
|
||||
return common.MUX_EXTENSION
|
||||
|
||||
def check_consistency_with_other_processors(self, processors):
|
||||
before_mux = True
|
||||
for processor in processors:
|
||||
name = processor.name()
|
||||
if name == self.name():
|
||||
before_mux = False
|
||||
continue
|
||||
if not processor.is_active():
|
||||
continue
|
||||
if before_mux:
|
||||
# Mux extension cannot be used after extensions
|
||||
# that depend on frame boundary, extension data field, or any
|
||||
# reserved bits which are attributed to each frame.
|
||||
if (name == common.DEFLATE_FRAME_EXTENSION or
|
||||
name == common.X_WEBKIT_DEFLATE_FRAME_EXTENSION):
|
||||
self.set_active(False)
|
||||
return
|
||||
else:
|
||||
# Mux extension should not be applied before any history-based
|
||||
# compression extension.
|
||||
if (name == 'deflate' or
|
||||
name == common.DEFLATE_FRAME_EXTENSION or
|
||||
name == common.X_WEBKIT_DEFLATE_FRAME_EXTENSION):
|
||||
self.set_active(False)
|
||||
return
|
||||
|
||||
def _get_extension_response_internal(self):
|
||||
self._active = False
|
||||
quota = self._request.get_parameter_value(self._QUOTA_PARAM)
|
||||
if quota is not None:
|
||||
try:
|
||||
quota = int(quota)
|
||||
except ValueError, e:
|
||||
return None
|
||||
if quota < 0 or quota >= 2 ** 32:
|
||||
return None
|
||||
self._quota = quota
|
||||
|
||||
self._active = True
|
||||
return common.ExtensionParameter(common.MUX_EXTENSION)
|
||||
|
||||
def _setup_stream_options_internal(self, stream_options):
|
||||
pass
|
||||
|
||||
def set_quota(self, quota):
|
||||
self._quota = quota
|
||||
|
||||
def quota(self):
|
||||
return self._quota
|
||||
|
||||
def set_extensions(self, extensions):
|
||||
self._extensions = extensions
|
||||
|
||||
def extensions(self):
|
||||
return self._extensions
|
||||
|
||||
|
||||
_available_processors[common.MUX_EXTENSION] = MuxExtensionProcessor
|
||||
PerMessageDeflateExtensionProcessor)
|
||||
|
||||
|
||||
def get_extension_processor(extension_request):
|
||||
|
@ -757,8 +470,4 @@ def get_extension_processor(extension_request):
|
|||
return processor_class(extension_request)
|
||||
|
||||
|
||||
def is_compression_extension(extension_name):
|
||||
return extension_name in _compression_extension_names
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -26,18 +26,15 @@
|
|||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""WebSocket opening handshake processor. This class try to apply available
|
||||
opening handshake processors for each protocol version until a connection is
|
||||
successfully established.
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket.handshake import hybi00
|
||||
from mod_pywebsocket.handshake import hybi
|
||||
# Export AbortedByUserException, HandshakeException, and VersionException
|
||||
# symbol from this module.
|
||||
|
@ -45,18 +42,15 @@ from mod_pywebsocket.handshake._base import AbortedByUserException
|
|||
from mod_pywebsocket.handshake._base import HandshakeException
|
||||
from mod_pywebsocket.handshake._base import VersionException
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def do_handshake(request, dispatcher, allowDraft75=False, strict=False):
|
||||
def do_handshake(request, dispatcher):
|
||||
"""Performs WebSocket handshake.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
dispatcher: Dispatcher (dispatch.Dispatcher).
|
||||
allowDraft75: obsolete argument. ignored.
|
||||
strict: obsolete argument. ignored.
|
||||
|
||||
Handshaker will add attributes such as ws_resource in performing
|
||||
handshake.
|
||||
|
@ -75,14 +69,11 @@ def do_handshake(request, dispatcher, allowDraft75=False, strict=False):
|
|||
# dict(mimetools.Message object) returns the map from header names to
|
||||
# header values. While MpTable_Type doesn't have such __str__ but just
|
||||
# __repr__ which formats itself as well as dictionary object.
|
||||
_LOGGER.debug(
|
||||
'Client\'s opening handshake headers: %r', dict(request.headers_in))
|
||||
_LOGGER.debug('Client\'s opening handshake headers: %r',
|
||||
dict(request.headers_in))
|
||||
|
||||
handshakers = []
|
||||
handshakers.append(
|
||||
('RFC 6455', hybi.Handshaker(request, dispatcher)))
|
||||
handshakers.append(
|
||||
('HyBi 00', hybi00.Handshaker(request, dispatcher)))
|
||||
handshakers.append(('RFC 6455', hybi.Handshaker(request, dispatcher)))
|
||||
|
||||
for name, handshaker in handshakers:
|
||||
_LOGGER.debug('Trying protocol version %s', name)
|
||||
|
@ -90,15 +81,15 @@ def do_handshake(request, dispatcher, allowDraft75=False, strict=False):
|
|||
handshaker.do_handshake()
|
||||
_LOGGER.info('Established (%s protocol)', name)
|
||||
return
|
||||
except HandshakeException, e:
|
||||
except HandshakeException as e:
|
||||
_LOGGER.debug(
|
||||
'Failed to complete opening handshake as %s protocol: %r',
|
||||
name, e)
|
||||
if e.status:
|
||||
raise e
|
||||
except AbortedByUserException, e:
|
||||
except AbortedByUserException as e:
|
||||
raise
|
||||
except VersionException, e:
|
||||
except VersionException as e:
|
||||
raise
|
||||
|
||||
# TODO(toyoshim): Add a test to cover the case all handshakers fail.
|
|
@ -26,13 +26,11 @@
|
|||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Common functions and exceptions used by WebSocket opening handshake
|
||||
processors.
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket import http_header_util
|
||||
|
||||
|
@ -55,7 +53,6 @@ class HandshakeException(Exception):
|
|||
"""This exception will be raised when an error occurred while processing
|
||||
WebSocket initial handshake.
|
||||
"""
|
||||
|
||||
def __init__(self, name, status=None):
|
||||
super(HandshakeException, self).__init__(name)
|
||||
self.status = status
|
||||
|
@ -65,13 +62,12 @@ class VersionException(Exception):
|
|||
"""This exception will be raised when a version of client request does not
|
||||
match with version the server supports.
|
||||
"""
|
||||
|
||||
def __init__(self, name, supported_versions=''):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
supported_version: a str object to show supported hybi versions.
|
||||
(e.g. '8, 13')
|
||||
(e.g. '13')
|
||||
"""
|
||||
super(VersionException, self).__init__(name)
|
||||
self.supported_versions = supported_versions
|
||||
|
@ -111,12 +107,12 @@ def parse_host_header(request):
|
|||
return fields[0], get_default_port(request.is_https())
|
||||
try:
|
||||
return fields[0], int(fields[1])
|
||||
except ValueError, e:
|
||||
except ValueError as e:
|
||||
raise HandshakeException('Invalid port number format: %r' % e)
|
||||
|
||||
|
||||
def format_header(name, value):
|
||||
return '%s: %s\r\n' % (name, value)
|
||||
return u'%s: %s\r\n' % (name, value)
|
||||
|
||||
|
||||
def get_mandatory_header(request, key):
|
||||
|
@ -132,16 +128,17 @@ def validate_mandatory_header(request, key, expected_value, fail_status=None):
|
|||
if value.lower() != expected_value.lower():
|
||||
raise HandshakeException(
|
||||
'Expected %r for header %s but found %r (case-insensitive)' %
|
||||
(expected_value, key, value), status=fail_status)
|
||||
(expected_value, key, value),
|
||||
status=fail_status)
|
||||
|
||||
|
||||
def check_request_line(request):
|
||||
# 5.1 1. The three character UTF-8 string "GET".
|
||||
# 5.1 2. A UTF-8-encoded U+0020 SPACE character (0x20 byte).
|
||||
if request.method != 'GET':
|
||||
if request.method != u'GET':
|
||||
raise HandshakeException('Method is not GET: %r' % request.method)
|
||||
|
||||
if request.protocol != 'HTTP/1.1':
|
||||
if request.protocol != u'HTTP/1.1':
|
||||
raise HandshakeException('Version is not HTTP/1.1: %r' %
|
||||
request.protocol)
|
||||
|
||||
|
@ -168,8 +165,8 @@ def parse_token_list(data):
|
|||
break
|
||||
|
||||
if not http_header_util.consume_string(state, ','):
|
||||
raise HandshakeException(
|
||||
'Expected a comma but found %r' % http_header_util.peek(state))
|
||||
raise HandshakeException('Expected a comma but found %r' %
|
||||
http_header_util.peek(state))
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
|
@ -26,8 +26,6 @@
|
|||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""This file provides the opening handshake processor for the WebSocket
|
||||
protocol (RFC 6455).
|
||||
|
||||
|
@ -35,21 +33,15 @@ Specification:
|
|||
http://tools.ietf.org/html/rfc6455
|
||||
"""
|
||||
|
||||
|
||||
# Note: request.connection.write is used in this module, even though mod_python
|
||||
# document says that it should be used only in connection handlers.
|
||||
# Unfortunately, we have no other options. For example, request.write is not
|
||||
# suitable because it doesn't allow direct raw bytes writing.
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from hashlib import sha1
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket.extensions import get_extension_processor
|
||||
from mod_pywebsocket.extensions import is_compression_extension
|
||||
from mod_pywebsocket.handshake._base import check_request_line
|
||||
from mod_pywebsocket.handshake._base import format_header
|
||||
from mod_pywebsocket.handshake._base import get_mandatory_header
|
||||
|
@ -61,7 +53,8 @@ from mod_pywebsocket.handshake._base import VersionException
|
|||
from mod_pywebsocket.stream import Stream
|
||||
from mod_pywebsocket.stream import StreamOptions
|
||||
from mod_pywebsocket import util
|
||||
|
||||
from six.moves import map
|
||||
from six.moves import range
|
||||
|
||||
# Used to validate the value in the Sec-WebSocket-Key header strictly. RFC 4648
|
||||
# disallows non-zero padding, so the character right before == must be any of
|
||||
|
@ -81,16 +74,24 @@ def compute_accept(key):
|
|||
Sec-WebSocket-Key header.
|
||||
"""
|
||||
|
||||
accept_binary = util.sha1_hash(
|
||||
key + common.WEBSOCKET_ACCEPT_UUID).digest()
|
||||
accept_binary = sha1(key + common.WEBSOCKET_ACCEPT_UUID).digest()
|
||||
accept = base64.b64encode(accept_binary)
|
||||
|
||||
return (accept, accept_binary)
|
||||
return accept
|
||||
|
||||
|
||||
def compute_accept_from_unicode(unicode_key):
|
||||
"""A wrapper function for compute_accept which takes a unicode string as an
|
||||
argument, and encodes it to byte string. It then passes it on to
|
||||
compute_accept.
|
||||
"""
|
||||
|
||||
key = unicode_key.encode('UTF-8')
|
||||
return compute_accept(key)
|
||||
|
||||
|
||||
class Handshaker(object):
|
||||
"""Opening handshake processor for the WebSocket protocol (RFC 6455)."""
|
||||
|
||||
def __init__(self, request, dispatcher):
|
||||
"""Construct an instance.
|
||||
|
||||
|
@ -107,14 +108,14 @@ class Handshaker(object):
|
|||
self._dispatcher = dispatcher
|
||||
|
||||
def _validate_connection_header(self):
|
||||
connection = get_mandatory_header(
|
||||
self._request, common.CONNECTION_HEADER)
|
||||
connection = get_mandatory_header(self._request,
|
||||
common.CONNECTION_HEADER)
|
||||
|
||||
try:
|
||||
connection_tokens = parse_token_list(connection)
|
||||
except HandshakeException, e:
|
||||
raise HandshakeException(
|
||||
'Failed to parse %s: %s' % (common.CONNECTION_HEADER, e))
|
||||
except HandshakeException as e:
|
||||
raise HandshakeException('Failed to parse %s: %s' %
|
||||
(common.CONNECTION_HEADER, e))
|
||||
|
||||
connection_is_valid = False
|
||||
for token in connection_tokens:
|
||||
|
@ -134,10 +135,8 @@ class Handshaker(object):
|
|||
|
||||
check_request_line(self._request)
|
||||
|
||||
validate_mandatory_header(
|
||||
self._request,
|
||||
common.UPGRADE_HEADER,
|
||||
common.WEBSOCKET_UPGRADE_TYPE)
|
||||
validate_mandatory_header(self._request, common.UPGRADE_HEADER,
|
||||
common.WEBSOCKET_UPGRADE_TYPE)
|
||||
|
||||
self._validate_connection_header()
|
||||
|
||||
|
@ -155,12 +154,10 @@ class Handshaker(object):
|
|||
# Key validation, response generation.
|
||||
|
||||
key = self._get_key()
|
||||
(accept, accept_binary) = compute_accept(key)
|
||||
self._logger.debug(
|
||||
'%s: %r (%s)',
|
||||
common.SEC_WEBSOCKET_ACCEPT_HEADER,
|
||||
accept,
|
||||
util.hexify(accept_binary))
|
||||
accept = compute_accept(key)
|
||||
self._logger.debug('%s: %r (%s)',
|
||||
common.SEC_WEBSOCKET_ACCEPT_HEADER, accept,
|
||||
util.hexify(base64.b64decode(accept)))
|
||||
|
||||
self._logger.debug('Protocol version is RFC 6455')
|
||||
|
||||
|
@ -182,8 +179,11 @@ class Handshaker(object):
|
|||
|
||||
# Extra handshake handler may modify/remove processors.
|
||||
self._dispatcher.do_extra_handshake(self._request)
|
||||
processors = filter(lambda processor: processor is not None,
|
||||
self._request.ws_extension_processors)
|
||||
processors = [
|
||||
processor
|
||||
for processor in self._request.ws_extension_processors
|
||||
if processor is not None
|
||||
]
|
||||
|
||||
# Ask each processor if there are extensions on the request which
|
||||
# cannot co-exist. When processor decided other processors cannot
|
||||
|
@ -194,35 +194,12 @@ class Handshaker(object):
|
|||
if processor.is_active():
|
||||
processor.check_consistency_with_other_processors(
|
||||
processors)
|
||||
processors = filter(lambda processor: processor.is_active(),
|
||||
processors)
|
||||
processors = [
|
||||
processor for processor in processors if processor.is_active()
|
||||
]
|
||||
|
||||
accepted_extensions = []
|
||||
|
||||
# We need to take into account of mux extension here.
|
||||
# If mux extension exists:
|
||||
# - Remove processors of extensions for logical channel,
|
||||
# which are processors located before the mux processor
|
||||
# - Pass extension requests for logical channel to mux processor
|
||||
# - Attach the mux processor to the request. It will be referred
|
||||
# by dispatcher to see whether the dispatcher should use mux
|
||||
# handler or not.
|
||||
mux_index = -1
|
||||
for i, processor in enumerate(processors):
|
||||
if processor.name() == common.MUX_EXTENSION:
|
||||
mux_index = i
|
||||
break
|
||||
if mux_index >= 0:
|
||||
logical_channel_extensions = []
|
||||
for processor in processors[:mux_index]:
|
||||
logical_channel_extensions.append(processor.request())
|
||||
processor.set_active(False)
|
||||
self._request.mux_processor = processors[mux_index]
|
||||
self._request.mux_processor.set_extensions(
|
||||
logical_channel_extensions)
|
||||
processors = filter(lambda processor: processor.is_active(),
|
||||
processors)
|
||||
|
||||
stream_options = StreamOptions()
|
||||
|
||||
for index, processor in enumerate(processors):
|
||||
|
@ -238,19 +215,17 @@ class Handshaker(object):
|
|||
|
||||
processor.setup_stream_options(stream_options)
|
||||
|
||||
if not is_compression_extension(processor.name()):
|
||||
continue
|
||||
|
||||
# Inactivate all of the following compression extensions.
|
||||
for j in xrange(index + 1, len(processors)):
|
||||
if is_compression_extension(processors[j].name()):
|
||||
processors[j].set_active(False)
|
||||
for j in range(index + 1, len(processors)):
|
||||
processors[j].set_active(False)
|
||||
|
||||
if len(accepted_extensions) > 0:
|
||||
self._request.ws_extensions = accepted_extensions
|
||||
self._logger.debug(
|
||||
'Extensions accepted: %r',
|
||||
map(common.ExtensionParameter.name, accepted_extensions))
|
||||
list(
|
||||
map(common.ExtensionParameter.name,
|
||||
accepted_extensions)))
|
||||
else:
|
||||
self._request.ws_extensions = None
|
||||
|
||||
|
@ -263,9 +238,8 @@ class Handshaker(object):
|
|||
'ws_requested_protocols and set it to ws_protocol')
|
||||
validate_subprotocol(self._request.ws_protocol)
|
||||
|
||||
self._logger.debug(
|
||||
'Subprotocol accepted: %r',
|
||||
self._request.ws_protocol)
|
||||
self._logger.debug('Subprotocol accepted: %r',
|
||||
self._request.ws_protocol)
|
||||
else:
|
||||
if self._request.ws_protocol is not None:
|
||||
raise HandshakeException(
|
||||
|
@ -273,7 +247,7 @@ class Handshaker(object):
|
|||
'request any subprotocol')
|
||||
|
||||
self._send_handshake(accept)
|
||||
except HandshakeException, e:
|
||||
except HandshakeException as e:
|
||||
if not e.status:
|
||||
# Fallback to 400 bad request by default.
|
||||
e.status = common.HTTP_STATUS_BAD_REQUEST
|
||||
|
@ -297,10 +271,10 @@ class Handshaker(object):
|
|||
'Multiple versions (%r) are not allowed for header %s' %
|
||||
(version, common.SEC_WEBSOCKET_VERSION_HEADER),
|
||||
status=common.HTTP_STATUS_BAD_REQUEST)
|
||||
raise VersionException(
|
||||
'Unsupported version %r for header %s' %
|
||||
(version, common.SEC_WEBSOCKET_VERSION_HEADER),
|
||||
supported_versions=', '.join(map(str, _SUPPORTED_VERSIONS)))
|
||||
raise VersionException('Unsupported version %r for header %s' %
|
||||
(version, common.SEC_WEBSOCKET_VERSION_HEADER),
|
||||
supported_versions=', '.join(
|
||||
map(str, _SUPPORTED_VERSIONS)))
|
||||
|
||||
def _set_protocol(self):
|
||||
self._request.ws_protocol = None
|
||||
|
@ -330,14 +304,15 @@ class Handshaker(object):
|
|||
try:
|
||||
self._request.ws_requested_extensions = common.parse_extensions(
|
||||
extensions_header)
|
||||
except common.ExtensionParsingException, e:
|
||||
except common.ExtensionParsingException as e:
|
||||
raise HandshakeException(
|
||||
'Failed to parse Sec-WebSocket-Extensions header: %r' % e)
|
||||
|
||||
self._logger.debug(
|
||||
'Extensions requested: %r',
|
||||
map(common.ExtensionParameter.name,
|
||||
self._request.ws_requested_extensions))
|
||||
list(
|
||||
map(common.ExtensionParameter.name,
|
||||
self._request.ws_requested_extensions)))
|
||||
|
||||
def _validate_key(self, key):
|
||||
if key.find(',') >= 0:
|
||||
|
@ -356,29 +331,25 @@ class Handshaker(object):
|
|||
decoded_key = base64.b64decode(key)
|
||||
if len(decoded_key) == 16:
|
||||
key_is_valid = True
|
||||
except TypeError, e:
|
||||
except TypeError as e:
|
||||
pass
|
||||
|
||||
if not key_is_valid:
|
||||
raise HandshakeException(
|
||||
'Illegal value for header %s: %r' %
|
||||
(common.SEC_WEBSOCKET_KEY_HEADER, key))
|
||||
raise HandshakeException('Illegal value for header %s: %r' %
|
||||
(common.SEC_WEBSOCKET_KEY_HEADER, key))
|
||||
|
||||
return decoded_key
|
||||
|
||||
def _get_key(self):
|
||||
key = get_mandatory_header(
|
||||
self._request, common.SEC_WEBSOCKET_KEY_HEADER)
|
||||
key = get_mandatory_header(self._request,
|
||||
common.SEC_WEBSOCKET_KEY_HEADER)
|
||||
|
||||
decoded_key = self._validate_key(key)
|
||||
|
||||
self._logger.debug(
|
||||
'%s: %r (%s)',
|
||||
common.SEC_WEBSOCKET_KEY_HEADER,
|
||||
key,
|
||||
util.hexify(decoded_key))
|
||||
self._logger.debug('%s: %r (%s)', common.SEC_WEBSOCKET_KEY_HEADER, key,
|
||||
util.hexify(decoded_key))
|
||||
|
||||
return key
|
||||
return key.encode('UTF-8')
|
||||
|
||||
def _create_stream(self, stream_options):
|
||||
return Stream(self._request, stream_options)
|
||||
|
@ -386,25 +357,29 @@ class Handshaker(object):
|
|||
def _create_handshake_response(self, accept):
|
||||
response = []
|
||||
|
||||
response.append('HTTP/1.1 101 Switching Protocols\r\n')
|
||||
response.append(u'HTTP/1.1 101 Switching Protocols\r\n')
|
||||
|
||||
# WebSocket headers
|
||||
response.append(format_header(
|
||||
common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE))
|
||||
response.append(format_header(
|
||||
common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE))
|
||||
response.append(format_header(
|
||||
common.SEC_WEBSOCKET_ACCEPT_HEADER, accept))
|
||||
response.append(
|
||||
format_header(common.UPGRADE_HEADER,
|
||||
common.WEBSOCKET_UPGRADE_TYPE))
|
||||
response.append(
|
||||
format_header(common.CONNECTION_HEADER,
|
||||
common.UPGRADE_CONNECTION_TYPE))
|
||||
response.append(
|
||||
format_header(common.SEC_WEBSOCKET_ACCEPT_HEADER,
|
||||
accept.decode('UTF-8')))
|
||||
if self._request.ws_protocol is not None:
|
||||
response.append(format_header(
|
||||
common.SEC_WEBSOCKET_PROTOCOL_HEADER,
|
||||
self._request.ws_protocol))
|
||||
if (self._request.ws_extensions is not None and
|
||||
len(self._request.ws_extensions) != 0):
|
||||
response.append(format_header(
|
||||
common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
|
||||
common.format_extensions(self._request.ws_extensions)))
|
||||
# MOZILLA: Add HSTS header if requested to
|
||||
response.append(
|
||||
format_header(common.SEC_WEBSOCKET_PROTOCOL_HEADER,
|
||||
self._request.ws_protocol))
|
||||
if (self._request.ws_extensions is not None
|
||||
and len(self._request.ws_extensions) != 0):
|
||||
response.append(
|
||||
format_header(
|
||||
common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
|
||||
common.format_extensions(self._request.ws_extensions)))
|
||||
# MOZILLA
|
||||
if self._request.sts is not None:
|
||||
response.append(format_header("Strict-Transport-Security",
|
||||
self._request.sts))
|
||||
|
@ -414,13 +389,13 @@ class Handshaker(object):
|
|||
for name, value in self._request.extra_headers:
|
||||
response.append(format_header(name, value))
|
||||
|
||||
response.append('\r\n')
|
||||
response.append(u'\r\n')
|
||||
|
||||
return ''.join(response)
|
||||
return u''.join(response)
|
||||
|
||||
def _send_handshake(self, accept):
|
||||
raw_response = self._create_handshake_response(accept)
|
||||
self._request.connection.write(raw_response)
|
||||
self._request.connection.write(raw_response.encode('UTF-8'))
|
||||
self._logger.debug('Sent server\'s opening handshake: %r',
|
||||
raw_response)
|
||||
|
|
@ -26,15 +26,12 @@
|
|||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Utilities for parsing and formatting headers that follow the grammar defined
|
||||
in HTTP RFC http://www.ietf.org/rfc/rfc2616.txt.
|
||||
"""
|
||||
|
||||
|
||||
import urlparse
|
||||
|
||||
from __future__ import absolute_import
|
||||
import six.moves.urllib.parse
|
||||
|
||||
_SEPARATORS = '()<>@,;:\\"/[]?={} \t'
|
||||
|
||||
|
@ -52,7 +49,6 @@ def _is_ctl(c):
|
|||
|
||||
|
||||
class ParsingState(object):
|
||||
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
self.head = 0
|
||||
|
@ -218,7 +214,7 @@ def quote_if_necessary(s):
|
|||
def parse_uri(uri):
|
||||
"""Parse absolute URI then return host, port and resource."""
|
||||
|
||||
parsed = urlparse.urlsplit(uri)
|
||||
parsed = six.moves.urllib.parse.urlsplit(uri)
|
||||
if parsed.scheme != 'wss' and parsed.scheme != 'ws':
|
||||
# |uri| must be a relative URI.
|
||||
# TODO(toyoshim): Should validate |uri|.
|
||||
|
@ -230,9 +226,12 @@ def parse_uri(uri):
|
|||
port = None
|
||||
try:
|
||||
port = parsed.port
|
||||
except ValueError, e:
|
||||
# port property cause ValueError on invalid null port description like
|
||||
# 'ws://host:/path'.
|
||||
except ValueError:
|
||||
# The port property cause ValueError on invalid null port descriptions
|
||||
# like 'ws://host:INVALID_PORT/path', where the assigned port is not
|
||||
# *DIGIT. For python 3.6 and later, ValueError also raises when
|
||||
# assigning invalid port numbers such as 'ws://host:-1/path'. Earlier
|
||||
# versions simply return None and ignore invalid port attributes.
|
||||
return None, None, None
|
||||
|
||||
if port is None:
|
||||
|
@ -252,12 +251,4 @@ def parse_uri(uri):
|
|||
return parsed.hostname, port, path
|
||||
|
||||
|
||||
try:
|
||||
urlparse.uses_netloc.index('ws')
|
||||
except ValueError, e:
|
||||
# urlparse in Python2.5.1 doesn't have 'ws' and 'wss' entries.
|
||||
urlparse.uses_netloc.append('ws')
|
||||
urlparse.uses_netloc.append('wss')
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -28,14 +28,12 @@
|
|||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Memorizing file.
|
||||
|
||||
A memorizing file wraps a file and memorizes lines read by readline.
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
import sys
|
||||
|
||||
|
||||
|
@ -46,8 +44,7 @@ class MemorizingFile(object):
|
|||
is good enough for memorizing lines SimpleHTTPServer reads before
|
||||
the control reaches WebSocketRequestHandler.
|
||||
"""
|
||||
|
||||
def __init__(self, file_, max_memorized_lines=sys.maxint):
|
||||
def __init__(self, file_, max_memorized_lines=sys.maxsize):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
|
@ -56,7 +53,6 @@ class MemorizingFile(object):
|
|||
Only the first max_memorized_lines are memorized.
|
||||
Default: sys.maxint.
|
||||
"""
|
||||
|
||||
self._file = file_
|
||||
self._memorized_lines = []
|
||||
self._max_memorized_lines = max_memorized_lines
|
||||
|
@ -64,6 +60,11 @@ class MemorizingFile(object):
|
|||
self._buffered_line = None
|
||||
|
||||
def __getattribute__(self, name):
|
||||
"""Return a file attribute.
|
||||
|
||||
Returns the value overridden by this class for some attributes,
|
||||
and forwards the call to _file for the other attributes.
|
||||
"""
|
||||
if name in ('_file', '_memorized_lines', '_max_memorized_lines',
|
||||
'_buffered', '_buffered_line', 'readline',
|
||||
'get_memorized_lines'):
|
||||
|
@ -77,7 +78,6 @@ class MemorizingFile(object):
|
|||
the whole line will be read out from underlying file object by
|
||||
subsequent readline calls.
|
||||
"""
|
||||
|
||||
if self._buffered:
|
||||
line = self._buffered_line
|
||||
self._buffered = False
|
|
@ -26,8 +26,6 @@
|
|||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Message related utilities.
|
||||
|
||||
Note: request.connection.write/read are used in this module, even though
|
||||
|
@ -37,16 +35,15 @@ request.write/read are not suitable because they don't allow direct raw
|
|||
bytes writing/reading.
|
||||
"""
|
||||
|
||||
|
||||
import Queue
|
||||
from __future__ import absolute_import
|
||||
import six.moves.queue
|
||||
import threading
|
||||
|
||||
|
||||
# Export Exception symbols from msgutil for backward compatibility
|
||||
from mod_pywebsocket._stream_base import ConnectionTerminatedException
|
||||
from mod_pywebsocket._stream_base import InvalidFrameException
|
||||
from mod_pywebsocket._stream_base import BadOperationException
|
||||
from mod_pywebsocket._stream_base import UnsupportedFrameException
|
||||
from mod_pywebsocket._stream_exceptions import ConnectionTerminatedException
|
||||
from mod_pywebsocket._stream_exceptions import InvalidFrameException
|
||||
from mod_pywebsocket._stream_exceptions import BadOperationException
|
||||
from mod_pywebsocket._stream_exceptions import UnsupportedFrameException
|
||||
|
||||
|
||||
# An API for handler to send/receive WebSocket messages.
|
||||
|
@ -95,7 +92,7 @@ def receive_message(request):
|
|||
return request.ws_stream.receive_message()
|
||||
|
||||
|
||||
def send_ping(request, body=''):
|
||||
def send_ping(request, body):
|
||||
request.ws_stream.send_ping(body)
|
||||
|
||||
|
||||
|
@ -109,7 +106,6 @@ 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.
|
||||
|
||||
|
@ -124,7 +120,7 @@ class MessageReceiver(threading.Thread):
|
|||
|
||||
threading.Thread.__init__(self)
|
||||
self._request = request
|
||||
self._queue = Queue.Queue()
|
||||
self._queue = six.moves.queue.Queue()
|
||||
self._onmessage = onmessage
|
||||
self._stop_requested = False
|
||||
self.setDaemon(True)
|
||||
|
@ -157,7 +153,7 @@ class MessageReceiver(threading.Thread):
|
|||
"""
|
||||
try:
|
||||
message = self._queue.get_nowait()
|
||||
except Queue.Empty:
|
||||
except six.moves.queue.Empty:
|
||||
message = None
|
||||
return message
|
||||
|
||||
|
@ -181,7 +177,6 @@ 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.
|
||||
|
||||
|
@ -190,7 +185,7 @@ class MessageSender(threading.Thread):
|
|||
"""
|
||||
threading.Thread.__init__(self)
|
||||
self._request = request
|
||||
self._queue = Queue.Queue()
|
||||
self._queue = six.moves.queue.Queue()
|
||||
self.setDaemon(True)
|
||||
self.start()
|
||||
|
|
@ -0,0 +1,319 @@
|
|||
# Copyright 2020, 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.
|
||||
"""Request Handler and Request/Connection classes for standalone server.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from six.moves import CGIHTTPServer
|
||||
from six.moves import http_client
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket import dispatch
|
||||
from mod_pywebsocket import handshake
|
||||
from mod_pywebsocket import http_header_util
|
||||
from mod_pywebsocket import memorizingfile
|
||||
from mod_pywebsocket import util
|
||||
|
||||
# 1024 is practically large enough to contain WebSocket handshake lines.
|
||||
_MAX_MEMORIZED_LINES = 1024
|
||||
|
||||
|
||||
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._logger = util.get_class_logger(self)
|
||||
|
||||
self._request_handler = request_handler
|
||||
self.connection = _StandaloneConnection(request_handler)
|
||||
self._use_tls = use_tls
|
||||
self.headers_in = request_handler.headers
|
||||
|
||||
def get_uri(self):
|
||||
"""Getter to mimic request.uri.
|
||||
|
||||
This method returns the raw data at the Request-URI part of the
|
||||
Request-Line, while the uri method on the request object of mod_python
|
||||
returns the path portion after parsing the raw data. This behavior is
|
||||
kept for compatibility.
|
||||
"""
|
||||
|
||||
return self._request_handler.path
|
||||
|
||||
uri = property(get_uri)
|
||||
|
||||
def get_unparsed_uri(self):
|
||||
"""Getter to mimic request.unparsed_uri."""
|
||||
|
||||
return self._request_handler.path
|
||||
|
||||
unparsed_uri = property(get_unparsed_uri)
|
||||
|
||||
def get_method(self):
|
||||
"""Getter to mimic request.method."""
|
||||
|
||||
return self._request_handler.command
|
||||
|
||||
method = property(get_method)
|
||||
|
||||
def get_protocol(self):
|
||||
"""Getter to mimic request.protocol."""
|
||||
|
||||
return self._request_handler.request_version
|
||||
|
||||
protocol = property(get_protocol)
|
||||
|
||||
def is_https(self):
|
||||
"""Mimic request.is_https()."""
|
||||
|
||||
return self._use_tls
|
||||
|
||||
|
||||
class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
|
||||
"""CGIHTTPRequestHandler specialized for WebSocket."""
|
||||
|
||||
# Use httplib.HTTPMessage instead of mimetools.Message.
|
||||
MessageClass = http_client.HTTPMessage
|
||||
|
||||
def setup(self):
|
||||
"""Override SocketServer.StreamRequestHandler.setup to wrap rfile
|
||||
with MemorizingFile.
|
||||
|
||||
This method will be called by BaseRequestHandler's constructor
|
||||
before calling BaseHTTPRequestHandler.handle.
|
||||
BaseHTTPRequestHandler.handle will call
|
||||
BaseHTTPRequestHandler.handle_one_request and it will call
|
||||
WebSocketRequestHandler.parse_request.
|
||||
"""
|
||||
|
||||
# Call superclass's setup to prepare rfile, wfile, etc. See setup
|
||||
# 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, request, client_address, server):
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._options = server.websocket_server_options
|
||||
|
||||
# Overrides CGIHTTPServerRequestHandler.cgi_directories.
|
||||
self.cgi_directories = self._options.cgi_directories
|
||||
# Replace CGIHTTPRequestHandler.is_executable method.
|
||||
if self._options.is_executable_method is not None:
|
||||
self.is_executable = self._options.is_executable_method
|
||||
|
||||
# This actually calls BaseRequestHandler.__init__.
|
||||
CGIHTTPServer.CGIHTTPRequestHandler.__init__(self, request,
|
||||
client_address, server)
|
||||
|
||||
def parse_request(self):
|
||||
"""Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request.
|
||||
|
||||
Return True to continue processing for HTTP(S), False otherwise.
|
||||
|
||||
See BaseHTTPRequestHandler.handle_one_request method which calls
|
||||
this method to understand how the return value will be handled.
|
||||
"""
|
||||
|
||||
# We hook parse_request method, but also call the original
|
||||
# CGIHTTPRequestHandler.parse_request since when we return False,
|
||||
# CGIHTTPRequestHandler.handle_one_request continues processing and
|
||||
# it needs variables set by CGIHTTPRequestHandler.parse_request.
|
||||
#
|
||||
# Variables set by this method will be also used by WebSocket request
|
||||
# handling (self.path, self.command, self.requestline, etc. See also
|
||||
# how _StandaloneRequest's members are implemented using these
|
||||
# attributes).
|
||||
if not CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self):
|
||||
return False
|
||||
|
||||
if self._options.use_basic_auth:
|
||||
auth = self.headers.get('Authorization')
|
||||
if auth != self._options.basic_auth_credential:
|
||||
self.send_response(401)
|
||||
self.send_header('WWW-Authenticate',
|
||||
'Basic realm="Pywebsocket"')
|
||||
self.end_headers()
|
||||
self._logger.info('Request basic authentication')
|
||||
return False
|
||||
|
||||
host, port, resource = http_header_util.parse_uri(self.path)
|
||||
if resource is None:
|
||||
self._logger.info('Invalid URI: %r', self.path)
|
||||
self._logger.info('Fallback to CGIHTTPRequestHandler')
|
||||
return True
|
||||
server_options = self.server.websocket_server_options
|
||||
if host is not None:
|
||||
validation_host = server_options.validation_host
|
||||
if validation_host is not None and host != validation_host:
|
||||
self._logger.info('Invalid host: %r (expected: %r)', host,
|
||||
validation_host)
|
||||
self._logger.info('Fallback to CGIHTTPRequestHandler')
|
||||
return True
|
||||
if port is not None:
|
||||
validation_port = server_options.validation_port
|
||||
if validation_port is not None and port != validation_port:
|
||||
self._logger.info('Invalid port: %r (expected: %r)', port,
|
||||
validation_port)
|
||||
self._logger.info('Fallback to CGIHTTPRequestHandler')
|
||||
return True
|
||||
self.path = resource
|
||||
|
||||
request = _StandaloneRequest(self, self._options.use_tls)
|
||||
|
||||
try:
|
||||
# Fallback to default http handler for request paths for which
|
||||
# we don't have request handlers.
|
||||
if not self._options.dispatcher.get_handler_suite(self.path):
|
||||
self._logger.info('No handler for resource: %r', self.path)
|
||||
self._logger.info('Fallback to CGIHTTPRequestHandler')
|
||||
return True
|
||||
except dispatch.DispatchException as e:
|
||||
self._logger.info('Dispatch failed for error: %s', e)
|
||||
self.send_error(e.status)
|
||||
return False
|
||||
|
||||
# If any Exceptions without except clause setup (including
|
||||
# DispatchException) is raised below this point, it will be caught
|
||||
# and logged by WebSocketServer.
|
||||
|
||||
try:
|
||||
try:
|
||||
handshake.do_handshake(request, self._options.dispatcher)
|
||||
except handshake.VersionException as e:
|
||||
self._logger.info('Handshake failed for version error: %s', e)
|
||||
self.send_response(common.HTTP_STATUS_BAD_REQUEST)
|
||||
self.send_header(common.SEC_WEBSOCKET_VERSION_HEADER,
|
||||
e.supported_versions)
|
||||
self.end_headers()
|
||||
return False
|
||||
except handshake.HandshakeException as e:
|
||||
# Handshake for ws(s) failed.
|
||||
self._logger.info('Handshake failed for error: %s', e)
|
||||
self.send_error(e.status)
|
||||
return False
|
||||
|
||||
request._dispatcher = self._options.dispatcher
|
||||
self._options.dispatcher.transfer_data(request)
|
||||
except handshake.AbortedByUserException as e:
|
||||
self._logger.info('Aborted: %s', e)
|
||||
return False
|
||||
|
||||
def log_request(self, code='-', size='-'):
|
||||
"""Override BaseHTTPServer.log_request."""
|
||||
|
||||
self._logger.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.
|
||||
self._logger.warning('%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
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -0,0 +1,87 @@
|
|||
# Copyright 2020, 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.
|
||||
"""Server related utilities."""
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
import threading
|
||||
import time
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket import util
|
||||
|
||||
|
||||
def _get_logger_from_class(c):
|
||||
return logging.getLogger('%s.%s' % (c.__module__, c.__name__))
|
||||
|
||||
|
||||
def configure_logging(options):
|
||||
logging.addLevelName(common.LOGLEVEL_FINE, 'FINE')
|
||||
|
||||
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)
|
||||
|
||||
deflate_log_level_name = logging.getLevelName(
|
||||
options.deflate_log_level.upper())
|
||||
_get_logger_from_class(util._Deflater).setLevel(deflate_log_level_name)
|
||||
_get_logger_from_class(util._Inflater).setLevel(deflate_log_level_name)
|
||||
|
||||
|
||||
class ThreadMonitor(threading.Thread):
|
||||
daemon = True
|
||||
|
||||
def __init__(self, interval_in_sec):
|
||||
threading.Thread.__init__(self, name='ThreadMonitor')
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._interval_in_sec = interval_in_sec
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
thread_name_list = []
|
||||
for thread in threading.enumerate():
|
||||
thread_name_list.append(thread.name)
|
||||
self._logger.info("%d active threads: %s",
|
||||
threading.active_count(),
|
||||
', '.join(thread_name_list))
|
||||
time.sleep(self._interval_in_sec)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -0,0 +1,483 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2012, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""Standalone WebSocket server.
|
||||
|
||||
Use this file to launch pywebsocket as a standalone server.
|
||||
|
||||
|
||||
BASIC USAGE
|
||||
===========
|
||||
|
||||
Go to the src directory and run
|
||||
|
||||
$ python mod_pywebsocket/standalone.py [-p <ws_port>]
|
||||
[-w <websock_handlers>]
|
||||
[-d <document_root>]
|
||||
|
||||
<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.
|
||||
If not specified, <document_root> will be used. See __init__.py (or
|
||||
run $ pydoc mod_pywebsocket) for how to write WebSocket handlers.
|
||||
|
||||
For more detail and other options, run
|
||||
|
||||
$ python mod_pywebsocket/standalone.py --help
|
||||
|
||||
or see _build_option_parser method below.
|
||||
|
||||
For trouble shooting, adding "--log_level debug" might help you.
|
||||
|
||||
|
||||
TRY DEMO
|
||||
========
|
||||
|
||||
Go to the src directory and run standalone.py with -d option to set the
|
||||
document root to the directory containing example HTMLs and handlers like this:
|
||||
|
||||
$ cd src
|
||||
$ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example
|
||||
|
||||
to launch pywebsocket with the sample handler and html on port 80. Open
|
||||
http://localhost/console.html, click the connect button, type something into
|
||||
the text box next to the send button and click the send button. If everything
|
||||
is working, you'll see the message you typed echoed by the server.
|
||||
|
||||
|
||||
USING TLS
|
||||
=========
|
||||
|
||||
To run the standalone server with TLS support, run it with -t, -k, and -c
|
||||
options. When TLS is enabled, the standalone server accepts only TLS connection.
|
||||
|
||||
Note that when ssl module is used and the key/cert location is incorrect,
|
||||
TLS connection silently fails while pyOpenSSL fails on startup.
|
||||
|
||||
Example:
|
||||
|
||||
$ PYTHONPATH=. python mod_pywebsocket/standalone.py \
|
||||
-d example \
|
||||
-p 10443 \
|
||||
-t \
|
||||
-c ../test/cert/cert.pem \
|
||||
-k ../test/cert/key.pem \
|
||||
|
||||
Note that when passing a relative path to -c and -k option, it will be resolved
|
||||
using the document root directory as the base.
|
||||
|
||||
|
||||
USING CLIENT AUTHENTICATION
|
||||
===========================
|
||||
|
||||
To run the standalone server with TLS client authentication support, run it with
|
||||
--tls-client-auth and --tls-client-ca options in addition to ones required for
|
||||
TLS support.
|
||||
|
||||
Example:
|
||||
|
||||
$ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example -p 10443 -t \
|
||||
-c ../test/cert/cert.pem -k ../test/cert/key.pem \
|
||||
--tls-client-auth \
|
||||
--tls-client-ca=../test/cert/cacert.pem
|
||||
|
||||
Note that when passing a relative path to --tls-client-ca option, it will be
|
||||
resolved using the document root directory as the base.
|
||||
|
||||
|
||||
CONFIGURATION FILE
|
||||
==================
|
||||
|
||||
You can also write a configuration file and use it by specifying the path to
|
||||
the configuration file by --config option. Please write a configuration file
|
||||
following the documentation of the Python ConfigParser library. Name of each
|
||||
entry must be the long version argument name. E.g. to set log level to debug,
|
||||
add the following line:
|
||||
|
||||
log_level=debug
|
||||
|
||||
For options which doesn't take value, please add some fake value. E.g. for
|
||||
--tls option, add the following line:
|
||||
|
||||
tls=True
|
||||
|
||||
Note that tls will be enabled even if you write tls=False as the value part is
|
||||
fake.
|
||||
|
||||
When both a command line argument and a configuration file entry are set for
|
||||
the same configuration item, the command line value will override one in the
|
||||
configuration file.
|
||||
|
||||
|
||||
THREADING
|
||||
=========
|
||||
|
||||
This server is derived from SocketServer.ThreadingMixIn. Hence a thread is
|
||||
used for each request.
|
||||
|
||||
|
||||
SECURITY WARNING
|
||||
================
|
||||
|
||||
This uses CGIHTTPServer and CGIHTTPServer is not secure.
|
||||
It may execute arbitrary Python code or external programs. It should not be
|
||||
used outside a firewall.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from six.moves import configparser
|
||||
import base64
|
||||
import logging
|
||||
import argparse
|
||||
import os
|
||||
import six
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket import util
|
||||
from mod_pywebsocket import server_util
|
||||
from mod_pywebsocket.websocket_server import WebSocketServer
|
||||
|
||||
_DEFAULT_LOG_MAX_BYTES = 1024 * 256
|
||||
_DEFAULT_LOG_BACKUP_COUNT = 5
|
||||
|
||||
_DEFAULT_REQUEST_QUEUE_SIZE = 128
|
||||
|
||||
|
||||
def _build_option_parser():
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument(
|
||||
'--config',
|
||||
dest='config_file',
|
||||
type=six.text_type,
|
||||
default=None,
|
||||
help=('Path to configuration file. See the file comment '
|
||||
'at the top of this file for the configuration '
|
||||
'file format'))
|
||||
parser.add_argument('-H',
|
||||
'--server-host',
|
||||
'--server_host',
|
||||
dest='server_host',
|
||||
default='',
|
||||
help='server hostname to listen to')
|
||||
parser.add_argument('-V',
|
||||
'--validation-host',
|
||||
'--validation_host',
|
||||
dest='validation_host',
|
||||
default=None,
|
||||
help='server hostname to validate in absolute path.')
|
||||
parser.add_argument('-p',
|
||||
'--port',
|
||||
dest='port',
|
||||
type=int,
|
||||
default=common.DEFAULT_WEB_SOCKET_PORT,
|
||||
help='port to listen to')
|
||||
parser.add_argument('-P',
|
||||
'--validation-port',
|
||||
'--validation_port',
|
||||
dest='validation_port',
|
||||
type=int,
|
||||
default=None,
|
||||
help='server port to validate in absolute path.')
|
||||
parser.add_argument(
|
||||
'-w',
|
||||
'--websock-handlers',
|
||||
'--websock_handlers',
|
||||
dest='websock_handlers',
|
||||
default='.',
|
||||
help=('The root directory of WebSocket handler files. '
|
||||
'If the path is relative, --document-root is used '
|
||||
'as the base.'))
|
||||
parser.add_argument('-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_argument('-s',
|
||||
'--scan-dir',
|
||||
'--scan_dir',
|
||||
dest='scan_dir',
|
||||
default=None,
|
||||
help=('Must be a directory under --websock-handlers. '
|
||||
'Only handlers under this directory are scanned '
|
||||
'and registered to the server. '
|
||||
'Useful for saving scan time when the handler '
|
||||
'root directory contains lots of files that are '
|
||||
'not handler file or are handler files but you '
|
||||
'don\'t want them to be registered. '))
|
||||
parser.add_argument(
|
||||
'--allow-handlers-outside-root-dir',
|
||||
'--allow_handlers_outside_root_dir',
|
||||
dest='allow_handlers_outside_root_dir',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help=('Scans WebSocket handlers even if their canonical '
|
||||
'path is not under --websock-handlers.'))
|
||||
parser.add_argument('-d',
|
||||
'--document-root',
|
||||
'--document_root',
|
||||
dest='document_root',
|
||||
default='.',
|
||||
help='Document root directory.')
|
||||
parser.add_argument('-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_argument('-t',
|
||||
'--tls',
|
||||
dest='use_tls',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='use TLS (wss://)')
|
||||
parser.add_argument('-k',
|
||||
'--private-key',
|
||||
'--private_key',
|
||||
dest='private_key',
|
||||
default='',
|
||||
help='TLS private key file.')
|
||||
parser.add_argument('-c',
|
||||
'--certificate',
|
||||
dest='certificate',
|
||||
default='',
|
||||
help='TLS certificate file.')
|
||||
parser.add_argument('--tls-client-auth',
|
||||
dest='tls_client_auth',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Requests TLS client auth on every connection.')
|
||||
parser.add_argument('--tls-client-cert-optional',
|
||||
dest='tls_client_cert_optional',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help=('Makes client certificate optional even though '
|
||||
'TLS client auth is enabled.'))
|
||||
parser.add_argument('--tls-client-ca',
|
||||
dest='tls_client_ca',
|
||||
default='',
|
||||
help=('Specifies a pem file which contains a set of '
|
||||
'concatenated CA certificates which are used to '
|
||||
'validate certificates passed from clients'))
|
||||
parser.add_argument('--basic-auth',
|
||||
dest='use_basic_auth',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Requires Basic authentication.')
|
||||
parser.add_argument(
|
||||
'--basic-auth-credential',
|
||||
dest='basic_auth_credential',
|
||||
default='test:test',
|
||||
help='Specifies the credential of basic authentication '
|
||||
'by username:password pair (e.g. test:test).')
|
||||
parser.add_argument('-l',
|
||||
'--log-file',
|
||||
'--log_file',
|
||||
dest='log_file',
|
||||
default='',
|
||||
help='Log file.')
|
||||
# Custom log level:
|
||||
# - FINE: Prints status of each frame processing step
|
||||
parser.add_argument('--log-level',
|
||||
'--log_level',
|
||||
type=six.text_type,
|
||||
dest='log_level',
|
||||
default='warn',
|
||||
choices=[
|
||||
'fine', 'debug', 'info', 'warning', 'warn',
|
||||
'error', 'critical'
|
||||
],
|
||||
help='Log level.')
|
||||
parser.add_argument(
|
||||
'--deflate-log-level',
|
||||
'--deflate_log_level',
|
||||
type=six.text_type,
|
||||
dest='deflate_log_level',
|
||||
default='warn',
|
||||
choices=['debug', 'info', 'warning', 'warn', 'error', 'critical'],
|
||||
help='Log level for _Deflater and _Inflater.')
|
||||
parser.add_argument('--thread-monitor-interval-in-sec',
|
||||
'--thread_monitor_interval_in_sec',
|
||||
dest='thread_monitor_interval_in_sec',
|
||||
type=int,
|
||||
default=-1,
|
||||
help=('If positive integer is specified, run a thread '
|
||||
'monitor to show the status of server threads '
|
||||
'periodically in the specified inteval in '
|
||||
'second. If non-positive integer is specified, '
|
||||
'disable the thread monitor.'))
|
||||
parser.add_argument('--log-max',
|
||||
'--log_max',
|
||||
dest='log_max',
|
||||
type=int,
|
||||
default=_DEFAULT_LOG_MAX_BYTES,
|
||||
help='Log maximum bytes')
|
||||
parser.add_argument('--log-count',
|
||||
'--log_count',
|
||||
dest='log_count',
|
||||
type=int,
|
||||
default=_DEFAULT_LOG_BACKUP_COUNT,
|
||||
help='Log backup count')
|
||||
parser.add_argument('-q',
|
||||
'--queue',
|
||||
dest='request_queue_size',
|
||||
type=int,
|
||||
default=_DEFAULT_REQUEST_QUEUE_SIZE,
|
||||
help='request queue size')
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def _parse_args_and_config(args):
|
||||
parser = _build_option_parser()
|
||||
|
||||
# First, parse options without configuration file.
|
||||
temporary_options, temporary_args = parser.parse_known_args(args=args)
|
||||
if temporary_args:
|
||||
logging.critical('Unrecognized positional arguments: %r',
|
||||
temporary_args)
|
||||
sys.exit(1)
|
||||
|
||||
if temporary_options.config_file:
|
||||
try:
|
||||
config_fp = open(temporary_options.config_file, 'r')
|
||||
except IOError as e:
|
||||
logging.critical('Failed to open configuration file %r: %r',
|
||||
temporary_options.config_file, e)
|
||||
sys.exit(1)
|
||||
|
||||
config_parser = configparser.SafeConfigParser()
|
||||
config_parser.readfp(config_fp)
|
||||
config_fp.close()
|
||||
|
||||
args_from_config = []
|
||||
for name, value in config_parser.items('pywebsocket'):
|
||||
args_from_config.append('--' + name)
|
||||
args_from_config.append(value)
|
||||
if args is None:
|
||||
args = args_from_config
|
||||
else:
|
||||
args = args_from_config + args
|
||||
return parser.parse_known_args(args=args)
|
||||
else:
|
||||
return temporary_options, temporary_args
|
||||
|
||||
|
||||
def _main(args=None):
|
||||
"""You can call this function from your own program, but please note that
|
||||
this function has some side-effects that might affect your program. For
|
||||
example, util.wrap_popen3_for_win use in this method replaces implementation
|
||||
of os.popen3.
|
||||
"""
|
||||
|
||||
options, args = _parse_args_and_config(args=args)
|
||||
|
||||
os.chdir(options.document_root)
|
||||
|
||||
server_util.configure_logging(options)
|
||||
|
||||
# TODO(tyoshino): Clean up initialization of CGI related values. Move some
|
||||
# of code here to WebSocketRequestHandler class if it's better.
|
||||
options.cgi_directories = []
|
||||
options.is_executable_method = None
|
||||
if options.cgi_paths:
|
||||
options.cgi_directories = options.cgi_paths.split(',')
|
||||
if sys.platform in ('cygwin', 'win32'):
|
||||
cygwin_path = None
|
||||
# For Win32 Python, it is expected that CYGWIN_PATH
|
||||
# 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)
|
||||
|
||||
options.is_executable_method = __check_script
|
||||
|
||||
if options.use_tls:
|
||||
logging.debug('Using ssl module')
|
||||
|
||||
if not options.private_key or not options.certificate:
|
||||
logging.critical(
|
||||
'To use TLS, specify private_key and certificate.')
|
||||
sys.exit(1)
|
||||
|
||||
if (options.tls_client_cert_optional and not options.tls_client_auth):
|
||||
logging.critical('Client authentication must be enabled to '
|
||||
'specify tls_client_cert_optional')
|
||||
sys.exit(1)
|
||||
else:
|
||||
if options.tls_client_auth:
|
||||
logging.critical('TLS must be enabled for client authentication.')
|
||||
sys.exit(1)
|
||||
|
||||
if options.tls_client_cert_optional:
|
||||
logging.critical('TLS must be enabled for client authentication.')
|
||||
sys.exit(1)
|
||||
|
||||
if not options.scan_dir:
|
||||
options.scan_dir = options.websock_handlers
|
||||
|
||||
if options.use_basic_auth:
|
||||
options.basic_auth_credential = 'Basic ' + base64.b64encode(
|
||||
options.basic_auth_credential.encode('UTF-8')).decode()
|
||||
|
||||
try:
|
||||
if options.thread_monitor_interval_in_sec > 0:
|
||||
# Run a thread monitor to show the status of server threads for
|
||||
# debugging.
|
||||
server_util.ThreadMonitor(
|
||||
options.thread_monitor_interval_in_sec).start()
|
||||
|
||||
server = WebSocketServer(options)
|
||||
server.serve_forever()
|
||||
except Exception as e:
|
||||
logging.critical('mod_pywebsocket: %s' % e)
|
||||
logging.critical('mod_pywebsocket: %s' % traceback.format_exc())
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_main(sys.argv[1:])
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright 2012, Google Inc.
|
||||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
|
@ -26,8 +26,6 @@
|
|||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""This file provides classes and helper functions for parsing/building frames
|
||||
of the WebSocket protocol (RFC 6455).
|
||||
|
||||
|
@ -35,30 +33,33 @@ Specification:
|
|||
http://tools.ietf.org/html/rfc6455
|
||||
"""
|
||||
|
||||
|
||||
from collections import deque
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
import time
|
||||
import socket
|
||||
import six
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket import util
|
||||
from mod_pywebsocket._stream_base import BadOperationException
|
||||
from mod_pywebsocket._stream_base import ConnectionTerminatedException
|
||||
from mod_pywebsocket._stream_base import InvalidFrameException
|
||||
from mod_pywebsocket._stream_base import InvalidUTF8Exception
|
||||
from mod_pywebsocket._stream_base import StreamBase
|
||||
from mod_pywebsocket._stream_base import UnsupportedFrameException
|
||||
|
||||
from mod_pywebsocket._stream_exceptions import BadOperationException
|
||||
from mod_pywebsocket._stream_exceptions import ConnectionTerminatedException
|
||||
from mod_pywebsocket._stream_exceptions import InvalidFrameException
|
||||
from mod_pywebsocket._stream_exceptions import InvalidUTF8Exception
|
||||
from mod_pywebsocket._stream_exceptions import UnsupportedFrameException
|
||||
|
||||
_NOOP_MASKER = util.NoopMasker()
|
||||
|
||||
|
||||
class Frame(object):
|
||||
|
||||
def __init__(self, fin=1, rsv1=0, rsv2=0, rsv3=0,
|
||||
opcode=None, payload=''):
|
||||
def __init__(self,
|
||||
fin=1,
|
||||
rsv1=0,
|
||||
rsv2=0,
|
||||
rsv3=0,
|
||||
opcode=None,
|
||||
payload=b''):
|
||||
self.fin = fin
|
||||
self.rsv1 = rsv1
|
||||
self.rsv2 = rsv2
|
||||
|
@ -90,11 +91,11 @@ def create_length_header(length, mask):
|
|||
if length < 0:
|
||||
raise ValueError('length must be non negative integer')
|
||||
elif length <= 125:
|
||||
return chr(mask_bit | length)
|
||||
return util.pack_byte(mask_bit | length)
|
||||
elif length < (1 << 16):
|
||||
return chr(mask_bit | 126) + struct.pack('!H', length)
|
||||
return util.pack_byte(mask_bit | 126) + struct.pack('!H', length)
|
||||
elif length < (1 << 63):
|
||||
return chr(mask_bit | 127) + struct.pack('!Q', length)
|
||||
return util.pack_byte(mask_bit | 127) + struct.pack('!Q', length)
|
||||
else:
|
||||
raise ValueError('Payload is too big for one frame')
|
||||
|
||||
|
@ -115,12 +116,12 @@ def create_header(opcode, payload_length, fin, rsv1, rsv2, rsv3, mask):
|
|||
if (fin | rsv1 | rsv2 | rsv3) & ~1:
|
||||
raise ValueError('FIN bit and Reserved bit parameter must be 0 or 1')
|
||||
|
||||
header = ''
|
||||
header = b''
|
||||
|
||||
first_byte = ((fin << 7)
|
||||
| (rsv1 << 6) | (rsv2 << 5) | (rsv3 << 4)
|
||||
| opcode)
|
||||
header += chr(first_byte)
|
||||
header += util.pack_byte(first_byte)
|
||||
header += create_length_header(payload_length, mask)
|
||||
|
||||
return header
|
||||
|
@ -140,22 +141,27 @@ def _filter_and_format_frame_object(frame, mask, frame_filters):
|
|||
for frame_filter in frame_filters:
|
||||
frame_filter.filter(frame)
|
||||
|
||||
header = create_header(
|
||||
frame.opcode, len(frame.payload), frame.fin,
|
||||
frame.rsv1, frame.rsv2, frame.rsv3, mask)
|
||||
header = create_header(frame.opcode, len(frame.payload), frame.fin,
|
||||
frame.rsv1, frame.rsv2, frame.rsv3, mask)
|
||||
return _build_frame(header, frame.payload, mask)
|
||||
|
||||
|
||||
def create_binary_frame(
|
||||
message, opcode=common.OPCODE_BINARY, fin=1, mask=False, frame_filters=[]):
|
||||
def create_binary_frame(message,
|
||||
opcode=common.OPCODE_BINARY,
|
||||
fin=1,
|
||||
mask=False,
|
||||
frame_filters=[]):
|
||||
"""Creates a simple binary frame with no extension, reserved bit."""
|
||||
|
||||
frame = Frame(fin=fin, opcode=opcode, payload=message)
|
||||
return _filter_and_format_frame_object(frame, mask, frame_filters)
|
||||
|
||||
|
||||
def create_text_frame(
|
||||
message, opcode=common.OPCODE_TEXT, fin=1, mask=False, frame_filters=[]):
|
||||
def create_text_frame(message,
|
||||
opcode=common.OPCODE_TEXT,
|
||||
fin=1,
|
||||
mask=False,
|
||||
frame_filters=[]):
|
||||
"""Creates a simple text frame with no extension, reserved bit."""
|
||||
|
||||
encoded_message = message.encode('utf-8')
|
||||
|
@ -163,7 +169,8 @@ def create_text_frame(
|
|||
frame_filters)
|
||||
|
||||
|
||||
def parse_frame(receive_bytes, logger=None,
|
||||
def parse_frame(receive_bytes,
|
||||
logger=None,
|
||||
ws_version=common.VERSION_HYBI_LATEST,
|
||||
unmask_receive=True):
|
||||
"""Parses a frame. Returns a tuple containing each header field and
|
||||
|
@ -189,23 +196,21 @@ def parse_frame(receive_bytes, logger=None,
|
|||
|
||||
logger.log(common.LOGLEVEL_FINE, 'Receive the first 2 octets of a frame')
|
||||
|
||||
received = receive_bytes(2)
|
||||
|
||||
first_byte = ord(received[0])
|
||||
first_byte = ord(receive_bytes(1))
|
||||
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])
|
||||
second_byte = ord(receive_bytes(1))
|
||||
mask = (second_byte >> 7) & 1
|
||||
payload_length = second_byte & 0x7f
|
||||
|
||||
logger.log(common.LOGLEVEL_FINE,
|
||||
'FIN=%s, RSV1=%s, RSV2=%s, RSV3=%s, opcode=%s, '
|
||||
'Mask=%s, Payload_length=%s',
|
||||
fin, rsv1, rsv2, rsv3, opcode, mask, payload_length)
|
||||
logger.log(
|
||||
common.LOGLEVEL_FINE, 'FIN=%s, RSV1=%s, RSV2=%s, RSV3=%s, opcode=%s, '
|
||||
'Mask=%s, Payload_length=%s', fin, rsv1, rsv2, rsv3, opcode, mask,
|
||||
payload_length)
|
||||
|
||||
if (mask == 1) != unmask_receive:
|
||||
raise InvalidFrameException(
|
||||
|
@ -222,36 +227,32 @@ def parse_frame(receive_bytes, logger=None,
|
|||
'Receive 8-octet extended payload length')
|
||||
|
||||
extended_payload_length = receive_bytes(8)
|
||||
payload_length = struct.unpack(
|
||||
'!Q', extended_payload_length)[0]
|
||||
payload_length = struct.unpack('!Q', extended_payload_length)[0]
|
||||
if payload_length > 0x7FFFFFFFFFFFFFFF:
|
||||
raise InvalidFrameException(
|
||||
'Extended payload length >= 2^63')
|
||||
raise InvalidFrameException('Extended payload length >= 2^63')
|
||||
if ws_version >= 13 and payload_length < 0x10000:
|
||||
valid_length_encoding = False
|
||||
length_encoding_bytes = 8
|
||||
|
||||
logger.log(common.LOGLEVEL_FINE,
|
||||
'Decoded_payload_length=%s', payload_length)
|
||||
logger.log(common.LOGLEVEL_FINE, 'Decoded_payload_length=%s',
|
||||
payload_length)
|
||||
elif payload_length == 126:
|
||||
logger.log(common.LOGLEVEL_FINE,
|
||||
'Receive 2-octet extended payload length')
|
||||
|
||||
extended_payload_length = receive_bytes(2)
|
||||
payload_length = struct.unpack(
|
||||
'!H', extended_payload_length)[0]
|
||||
payload_length = struct.unpack('!H', extended_payload_length)[0]
|
||||
if ws_version >= 13 and payload_length < 126:
|
||||
valid_length_encoding = False
|
||||
length_encoding_bytes = 2
|
||||
|
||||
logger.log(common.LOGLEVEL_FINE,
|
||||
'Decoded_payload_length=%s', payload_length)
|
||||
logger.log(common.LOGLEVEL_FINE, 'Decoded_payload_length=%s',
|
||||
payload_length)
|
||||
|
||||
if not valid_length_encoding:
|
||||
logger.warning(
|
||||
'Payload length is not encoded using the minimal number of '
|
||||
'bytes (%d is encoded using %d bytes)',
|
||||
payload_length,
|
||||
'bytes (%d is encoded using %d bytes)', payload_length,
|
||||
length_encoding_bytes)
|
||||
|
||||
if mask == 1:
|
||||
|
@ -272,8 +273,7 @@ def parse_frame(receive_bytes, logger=None,
|
|||
|
||||
if logger.isEnabledFor(common.LOGLEVEL_FINE):
|
||||
logger.log(
|
||||
common.LOGLEVEL_FINE,
|
||||
'Done receiving payload data at %s MB/s',
|
||||
common.LOGLEVEL_FINE, 'Done receiving payload data at %s MB/s',
|
||||
payload_length / (time.time() - receive_start) / 1000 / 1000)
|
||||
logger.log(common.LOGLEVEL_FINE, 'Unmask payload data')
|
||||
|
||||
|
@ -283,17 +283,15 @@ def parse_frame(receive_bytes, logger=None,
|
|||
unmasked_bytes = masker.mask(raw_payload_bytes)
|
||||
|
||||
if logger.isEnabledFor(common.LOGLEVEL_FINE):
|
||||
logger.log(
|
||||
common.LOGLEVEL_FINE,
|
||||
'Done unmasking payload data at %s MB/s',
|
||||
payload_length / (time.time() - unmask_start) / 1000 / 1000)
|
||||
logger.log(common.LOGLEVEL_FINE,
|
||||
'Done unmasking payload data at %s MB/s',
|
||||
payload_length / (time.time() - unmask_start) / 1000 / 1000)
|
||||
|
||||
return opcode, unmasked_bytes, fin, rsv1, rsv2, rsv3
|
||||
|
||||
|
||||
class FragmentedFrameBuilder(object):
|
||||
"""A stateful class to send a message as fragments."""
|
||||
|
||||
def __init__(self, mask, frame_filters=[], encode_utf8=True):
|
||||
"""Constructs an instance."""
|
||||
|
||||
|
@ -331,11 +329,11 @@ class FragmentedFrameBuilder(object):
|
|||
fin = 0
|
||||
|
||||
if binary or not self._encode_utf8:
|
||||
return create_binary_frame(
|
||||
payload_data, opcode, fin, self._mask, self._frame_filters)
|
||||
return create_binary_frame(payload_data, opcode, fin, self._mask,
|
||||
self._frame_filters)
|
||||
else:
|
||||
return create_text_frame(
|
||||
payload_data, opcode, fin, self._mask, self._frame_filters)
|
||||
return create_text_frame(payload_data, opcode, fin, self._mask,
|
||||
self._frame_filters)
|
||||
|
||||
|
||||
def _create_control_frame(opcode, body, mask, frame_filters):
|
||||
|
@ -348,9 +346,8 @@ def _create_control_frame(opcode, body, mask, frame_filters):
|
|||
raise BadOperationException(
|
||||
'Payload data size of control frames must be 125 bytes or less')
|
||||
|
||||
header = create_header(
|
||||
frame.opcode, len(frame.payload), frame.fin,
|
||||
frame.rsv1, frame.rsv2, frame.rsv3, mask)
|
||||
header = create_header(frame.opcode, len(frame.payload), frame.fin,
|
||||
frame.rsv1, frame.rsv2, frame.rsv3, mask)
|
||||
return _build_frame(header, frame.payload, mask)
|
||||
|
||||
|
||||
|
@ -363,21 +360,21 @@ def create_pong_frame(body, mask=False, frame_filters=[]):
|
|||
|
||||
|
||||
def create_close_frame(body, mask=False, frame_filters=[]):
|
||||
return _create_control_frame(
|
||||
common.OPCODE_CLOSE, body, mask, frame_filters)
|
||||
return _create_control_frame(common.OPCODE_CLOSE, body, mask,
|
||||
frame_filters)
|
||||
|
||||
|
||||
def create_closing_handshake_body(code, reason):
|
||||
body = ''
|
||||
body = b''
|
||||
if code is not None:
|
||||
if (code > common.STATUS_USER_PRIVATE_MAX or
|
||||
code < common.STATUS_NORMAL_CLOSURE):
|
||||
if (code > common.STATUS_USER_PRIVATE_MAX
|
||||
or code < common.STATUS_NORMAL_CLOSURE):
|
||||
raise BadOperationException('Status code is out of range')
|
||||
if (code == common.STATUS_NO_STATUS_RECEIVED or
|
||||
code == common.STATUS_ABNORMAL_CLOSURE or
|
||||
code == common.STATUS_TLS_HANDSHAKE):
|
||||
if (code == common.STATUS_NO_STATUS_RECEIVED
|
||||
or code == common.STATUS_ABNORMAL_CLOSURE
|
||||
or code == common.STATUS_TLS_HANDSHAKE):
|
||||
raise BadOperationException('Status code is reserved pseudo '
|
||||
'code')
|
||||
'code')
|
||||
encoded_reason = reason.encode('utf-8')
|
||||
body = struct.pack('!H', code) + encoded_reason
|
||||
return body
|
||||
|
@ -385,7 +382,6 @@ def create_closing_handshake_body(code, reason):
|
|||
|
||||
class StreamOptions(object):
|
||||
"""Holds option values to configure Stream objects."""
|
||||
|
||||
def __init__(self):
|
||||
"""Constructs StreamOptions."""
|
||||
|
||||
|
@ -402,11 +398,10 @@ class StreamOptions(object):
|
|||
self.unmask_receive = True
|
||||
|
||||
|
||||
class Stream(StreamBase):
|
||||
class Stream(object):
|
||||
"""A class for parsing/building frames of the WebSocket protocol
|
||||
(RFC 6455).
|
||||
"""
|
||||
|
||||
def __init__(self, request, options):
|
||||
"""Constructs an instance.
|
||||
|
||||
|
@ -414,11 +409,10 @@ class Stream(StreamBase):
|
|||
request: mod_python request.
|
||||
"""
|
||||
|
||||
StreamBase.__init__(self, request)
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._options = options
|
||||
self._request = request
|
||||
|
||||
self._request.client_terminated = False
|
||||
self._request.server_terminated = False
|
||||
|
@ -434,6 +428,71 @@ class Stream(StreamBase):
|
|||
|
||||
self._ping_queue = deque()
|
||||
|
||||
def _read(self, length):
|
||||
"""Reads length bytes from connection. In case we catch any exception,
|
||||
prepends remote address to the exception message and raise again.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty string.
|
||||
"""
|
||||
|
||||
try:
|
||||
read_bytes = self._request.connection.read(length)
|
||||
if not read_bytes:
|
||||
raise ConnectionTerminatedException(
|
||||
'Receiving %d byte failed. Peer (%r) closed connection' %
|
||||
(length, (self._request.connection.remote_addr, )))
|
||||
return read_bytes
|
||||
except IOError as e:
|
||||
# Also catch an IOError because mod_python throws it.
|
||||
raise ConnectionTerminatedException(
|
||||
'Receiving %d byte failed. IOError (%s) occurred' %
|
||||
(length, e))
|
||||
|
||||
def _write(self, bytes_to_write):
|
||||
"""Writes given bytes to connection. In case we catch any exception,
|
||||
prepends remote address to the exception message and raise again.
|
||||
"""
|
||||
|
||||
try:
|
||||
self._request.connection.write(bytes_to_write)
|
||||
except Exception as 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. This method returns byte strings.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty string.
|
||||
"""
|
||||
|
||||
read_bytes = []
|
||||
while length > 0:
|
||||
new_read_bytes = self._read(length)
|
||||
read_bytes.append(new_read_bytes)
|
||||
length -= len(new_read_bytes)
|
||||
return b''.join(read_bytes)
|
||||
|
||||
def _read_until(self, delim_char):
|
||||
"""Reads bytes until we encounter delim_char. The result will not
|
||||
contain delim_char.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when read returns empty string.
|
||||
"""
|
||||
|
||||
read_bytes = []
|
||||
while True:
|
||||
ch = self._read(1)
|
||||
if ch == delim_char:
|
||||
break
|
||||
read_bytes.append(ch)
|
||||
return b''.join(read_bytes)
|
||||
|
||||
def _receive_frame(self):
|
||||
"""Receives a frame and return data in the frame as a tuple containing
|
||||
each header field and payload separately.
|
||||
|
@ -443,7 +502,6 @@ class Stream(StreamBase):
|
|||
string.
|
||||
InvalidFrameException: when the frame contains invalid data.
|
||||
"""
|
||||
|
||||
def _receive_bytes(length):
|
||||
return self.receive_bytes(length)
|
||||
|
||||
|
@ -455,8 +513,12 @@ class Stream(StreamBase):
|
|||
def _receive_frame_as_frame_object(self):
|
||||
opcode, unmasked_bytes, fin, rsv1, rsv2, rsv3 = self._receive_frame()
|
||||
|
||||
return Frame(fin=fin, rsv1=rsv1, rsv2=rsv2, rsv3=rsv3,
|
||||
opcode=opcode, payload=unmasked_bytes)
|
||||
return Frame(fin=fin,
|
||||
rsv1=rsv1,
|
||||
rsv2=rsv2,
|
||||
rsv3=rsv3,
|
||||
opcode=opcode,
|
||||
payload=unmasked_bytes)
|
||||
|
||||
def receive_filtered_frame(self):
|
||||
"""Receives a frame and applies frame filters and message filters.
|
||||
|
@ -472,8 +534,8 @@ class Stream(StreamBase):
|
|||
raise InvalidFrameException(
|
||||
'Segmented frames must not be received via '
|
||||
'receive_filtered_frame()')
|
||||
if (frame.opcode != common.OPCODE_TEXT and
|
||||
frame.opcode != common.OPCODE_BINARY):
|
||||
if (frame.opcode != common.OPCODE_TEXT
|
||||
and frame.opcode != common.OPCODE_BINARY):
|
||||
raise InvalidFrameException(
|
||||
'Control frames must not be received via '
|
||||
'receive_filtered_frame()')
|
||||
|
@ -501,9 +563,9 @@ class Stream(StreamBase):
|
|||
raise BadOperationException(
|
||||
'Requested send_message after sending out a closing handshake')
|
||||
|
||||
if binary and isinstance(message, unicode):
|
||||
if binary and isinstance(message, six.text_type):
|
||||
raise BadOperationException(
|
||||
'Message for binary frame must be instance of str')
|
||||
'Message for binary frame must not be instance of Unicode')
|
||||
|
||||
for message_filter in self._options.outgoing_message_filters:
|
||||
message = message_filter.filter(message, end, binary)
|
||||
|
@ -521,15 +583,14 @@ class Stream(StreamBase):
|
|||
while True:
|
||||
end_for_this_frame = end
|
||||
bytes_to_write = len(message) - bytes_written
|
||||
if (MAX_PAYLOAD_DATA_SIZE > 0 and
|
||||
bytes_to_write > MAX_PAYLOAD_DATA_SIZE):
|
||||
if (MAX_PAYLOAD_DATA_SIZE > 0
|
||||
and bytes_to_write > MAX_PAYLOAD_DATA_SIZE):
|
||||
end_for_this_frame = False
|
||||
bytes_to_write = MAX_PAYLOAD_DATA_SIZE
|
||||
|
||||
frame = self._writer.build(
|
||||
message[bytes_written:bytes_written + bytes_to_write],
|
||||
end_for_this_frame,
|
||||
binary)
|
||||
end_for_this_frame, binary)
|
||||
self._write(frame)
|
||||
|
||||
bytes_written += bytes_to_write
|
||||
|
@ -538,7 +599,7 @@ class Stream(StreamBase):
|
|||
# at least one frame is sent.
|
||||
if len(message) <= bytes_written:
|
||||
break
|
||||
except ValueError, e:
|
||||
except ValueError as e:
|
||||
raise BadOperationException(e)
|
||||
|
||||
def _get_message_from_frame(self, frame):
|
||||
|
@ -566,7 +627,7 @@ class Stream(StreamBase):
|
|||
if frame.fin:
|
||||
# End of fragmentation frame
|
||||
self._received_fragments.append(frame.payload)
|
||||
message = ''.join(self._received_fragments)
|
||||
message = b''.join(self._received_fragments)
|
||||
self._received_fragments = []
|
||||
return message
|
||||
else:
|
||||
|
@ -620,21 +681,18 @@ class Stream(StreamBase):
|
|||
# - 3 or more octet of application data: both code and reason
|
||||
if len(message) == 0:
|
||||
self._logger.debug('Received close frame (empty body)')
|
||||
self._request.ws_close_code = (
|
||||
common.STATUS_NO_STATUS_RECEIVED)
|
||||
self._request.ws_close_code = common.STATUS_NO_STATUS_RECEIVED
|
||||
elif len(message) == 1:
|
||||
raise InvalidFrameException(
|
||||
'If a close frame has status code, the length of '
|
||||
'status code must be 2 octet')
|
||||
elif len(message) >= 2:
|
||||
self._request.ws_close_code = struct.unpack(
|
||||
'!H', message[0:2])[0]
|
||||
self._request.ws_close_code = struct.unpack('!H', message[0:2])[0]
|
||||
self._request.ws_close_reason = message[2:].decode(
|
||||
'utf-8', 'replace')
|
||||
self._logger.debug(
|
||||
'Received close frame (code=%d, reason=%r)',
|
||||
self._request.ws_close_code,
|
||||
self._request.ws_close_reason)
|
||||
self._logger.debug('Received close frame (code=%d, reason=%r)',
|
||||
self._request.ws_close_code,
|
||||
self._request.ws_close_reason)
|
||||
|
||||
# As we've received a close frame, no more data is coming over the
|
||||
# socket. We can now safely close the socket without worrying about
|
||||
|
@ -645,15 +703,13 @@ class Stream(StreamBase):
|
|||
'Received ack for server-initiated closing handshake')
|
||||
return
|
||||
|
||||
self._logger.debug(
|
||||
'Received client-initiated closing handshake')
|
||||
self._logger.debug('Received client-initiated closing handshake')
|
||||
|
||||
code = common.STATUS_NORMAL_CLOSURE
|
||||
reason = ''
|
||||
if hasattr(self._request, '_dispatcher'):
|
||||
dispatcher = self._request._dispatcher
|
||||
code, reason = dispatcher.passive_closing_handshake(
|
||||
self._request)
|
||||
code, reason = dispatcher.passive_closing_handshake(self._request)
|
||||
if code is None and reason is not None and len(reason) > 0:
|
||||
self._logger.warning(
|
||||
'Handler specified reason despite code being None')
|
||||
|
@ -677,7 +733,7 @@ class Stream(StreamBase):
|
|||
if handler:
|
||||
handler(self._request, message)
|
||||
return
|
||||
except AttributeError, e:
|
||||
except AttributeError:
|
||||
pass
|
||||
self._send_pong(message)
|
||||
|
||||
|
@ -704,7 +760,7 @@ class Stream(StreamBase):
|
|||
break
|
||||
else:
|
||||
inflight_pings.append(expected_body)
|
||||
except IndexError, e:
|
||||
except IndexError:
|
||||
# The received pong was unsolicited pong. Keep the
|
||||
# ping queue as is.
|
||||
self._ping_queue = inflight_pings
|
||||
|
@ -715,7 +771,7 @@ class Stream(StreamBase):
|
|||
handler = self._request.on_pong_handler
|
||||
if handler:
|
||||
handler(self._request, message)
|
||||
except AttributeError, e:
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def receive_message(self):
|
||||
|
@ -746,15 +802,14 @@ class Stream(StreamBase):
|
|||
|
||||
while True:
|
||||
# mp_conn.read will block if no bytes are available.
|
||||
# Timeout is controlled by TimeOut directive of Apache.
|
||||
|
||||
frame = self._receive_frame_as_frame_object()
|
||||
|
||||
# Check the constraint on the payload size for control frames
|
||||
# before extension processes the frame.
|
||||
# See also http://tools.ietf.org/html/rfc6455#section-5.5
|
||||
if (common.is_control_opcode(frame.opcode) and
|
||||
len(frame.payload) > 125):
|
||||
if (common.is_control_opcode(frame.opcode)
|
||||
and len(frame.payload) > 125):
|
||||
raise InvalidFrameException(
|
||||
'Payload data size of control frames must be 125 bytes or '
|
||||
'less')
|
||||
|
@ -780,7 +835,7 @@ class Stream(StreamBase):
|
|||
# CHARACTER.
|
||||
try:
|
||||
return message.decode('utf-8')
|
||||
except UnicodeDecodeError, e:
|
||||
except UnicodeDecodeError as e:
|
||||
raise InvalidUTF8Exception(e)
|
||||
elif self._original_opcode == common.OPCODE_BINARY:
|
||||
return message
|
||||
|
@ -792,20 +847,23 @@ class Stream(StreamBase):
|
|||
elif self._original_opcode == common.OPCODE_PONG:
|
||||
self._process_pong_message(message)
|
||||
else:
|
||||
raise UnsupportedFrameException(
|
||||
'Opcode %d is not supported' % self._original_opcode)
|
||||
raise UnsupportedFrameException('Opcode %d is not supported' %
|
||||
self._original_opcode)
|
||||
|
||||
def _send_closing_handshake(self, code, reason):
|
||||
body = create_closing_handshake_body(code, reason)
|
||||
frame = create_close_frame(
|
||||
body, mask=self._options.mask_send,
|
||||
body,
|
||||
mask=self._options.mask_send,
|
||||
frame_filters=self._options.outgoing_frame_filters)
|
||||
|
||||
self._request.server_terminated = True
|
||||
|
||||
self._write(frame)
|
||||
|
||||
def close_connection(self, code=common.STATUS_NORMAL_CLOSURE, reason='',
|
||||
def close_connection(self,
|
||||
code=common.STATUS_NORMAL_CLOSURE,
|
||||
reason='',
|
||||
wait_response=True):
|
||||
"""Closes a WebSocket connection. Note that this method blocks until
|
||||
it receives acknowledgement to the closing handshake.
|
||||
|
@ -837,17 +895,17 @@ class Stream(StreamBase):
|
|||
'close reason must not be specified if code is None')
|
||||
reason = ''
|
||||
else:
|
||||
if not isinstance(reason, str) and not isinstance(reason, unicode):
|
||||
if not isinstance(reason, bytes) and not isinstance(
|
||||
reason, six.text_type):
|
||||
raise BadOperationException(
|
||||
'close reason must be an instance of str or unicode')
|
||||
'close reason must be an instance of bytes or unicode')
|
||||
|
||||
self._send_closing_handshake(code, reason)
|
||||
self._logger.debug(
|
||||
'Initiated closing handshake (code=%r, reason=%r)',
|
||||
code, reason)
|
||||
self._logger.debug('Initiated closing handshake (code=%r, reason=%r)',
|
||||
code, reason)
|
||||
|
||||
if (code == common.STATUS_GOING_AWAY or
|
||||
code == common.STATUS_PROTOCOL_ERROR) or not wait_response:
|
||||
if (code == common.STATUS_GOING_AWAY
|
||||
or code == common.STATUS_PROTOCOL_ERROR) or not wait_response:
|
||||
# It doesn't make sense to wait for a close frame if the reason is
|
||||
# protocol error or that the server is going away. For some of
|
||||
# other reasons, it might not make sense to wait for a close frame,
|
||||
|
@ -866,20 +924,18 @@ class Stream(StreamBase):
|
|||
# TODO: 3. close the WebSocket connection.
|
||||
# note: mod_python Connection (mp_conn) doesn't have close method.
|
||||
|
||||
def send_ping(self, body=''):
|
||||
frame = create_ping_frame(
|
||||
body,
|
||||
self._options.mask_send,
|
||||
self._options.outgoing_frame_filters)
|
||||
def send_ping(self, body, binary=False):
|
||||
if not binary and isinstance(body, six.text_type):
|
||||
body = body.encode('UTF-8')
|
||||
frame = create_ping_frame(body, self._options.mask_send,
|
||||
self._options.outgoing_frame_filters)
|
||||
self._write(frame)
|
||||
|
||||
self._ping_queue.append(body)
|
||||
|
||||
def _send_pong(self, body):
|
||||
frame = create_pong_frame(
|
||||
body,
|
||||
self._options.mask_send,
|
||||
self._options.outgoing_frame_filters)
|
||||
frame = create_pong_frame(body, self._options.mask_send,
|
||||
self._options.outgoing_frame_filters)
|
||||
self._write(frame)
|
||||
|
||||
def get_last_received_opcode(self):
|
|
@ -26,34 +26,19 @@
|
|||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""WebSocket utilities."""
|
||||
|
||||
|
||||
"""WebSocket utilities.
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
import array
|
||||
import errno
|
||||
|
||||
# Import hash classes from a module available and recommended for each Python
|
||||
# version and re-export those symbol. Use sha and md5 module in Python 2.4, and
|
||||
# hashlib module in Python 2.6.
|
||||
try:
|
||||
import hashlib
|
||||
md5_hash = hashlib.md5
|
||||
sha1_hash = hashlib.sha1
|
||||
except ImportError:
|
||||
import md5
|
||||
import sha
|
||||
md5_hash = md5.md5
|
||||
sha1_hash = sha.sha
|
||||
|
||||
import StringIO
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import six
|
||||
from six.moves import map
|
||||
from six.moves import range
|
||||
import socket
|
||||
import traceback
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
try:
|
||||
|
@ -62,23 +47,9 @@ except ImportError:
|
|||
pass
|
||||
|
||||
|
||||
def get_stack_trace():
|
||||
"""Get the current stack trace as string.
|
||||
|
||||
This is needed to support Python 2.3.
|
||||
TODO: Remove this when we only support Python 2.4 and above.
|
||||
Use traceback.format_exc instead.
|
||||
"""
|
||||
|
||||
out = StringIO.StringIO()
|
||||
traceback.print_exc(file=out)
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def prepend_message_to_exception(message, exc):
|
||||
"""Prepend message to the exception."""
|
||||
|
||||
exc.args = (message + str(exc),)
|
||||
exc.args = (message + str(exc), )
|
||||
return
|
||||
|
||||
|
||||
|
@ -104,7 +75,7 @@ def __translate_interp(interp, cygwin_path):
|
|||
|
||||
|
||||
def get_script_interp(script_path, cygwin_path=None):
|
||||
"""Gets #!-interpreter command line from the script.
|
||||
r"""Get #!-interpreter command line from the script.
|
||||
|
||||
It also fixes command path. When Cygwin Python is used, e.g. in WebKit,
|
||||
it could run "/usr/bin/perl -wT hello.pl".
|
||||
|
@ -133,7 +104,6 @@ def wrap_popen3_for_win(cygwin_path):
|
|||
cygwin_path: path for cygwin binary if command path is needed to be
|
||||
translated. None if no translation required.
|
||||
"""
|
||||
|
||||
__orig_popen3 = os.popen3
|
||||
|
||||
def __wrap_popen3(cmd, mode='t', bufsize=-1):
|
||||
|
@ -147,61 +117,76 @@ def wrap_popen3_for_win(cygwin_path):
|
|||
|
||||
|
||||
def hexify(s):
|
||||
return ' '.join(map(lambda x: '%02x' % ord(x), s))
|
||||
return ' '.join(['%02x' % x for x in six.iterbytes(s)])
|
||||
|
||||
|
||||
def get_class_logger(o):
|
||||
return logging.getLogger(
|
||||
'%s.%s' % (o.__class__.__module__, o.__class__.__name__))
|
||||
"""Return the logging class information."""
|
||||
return logging.getLogger('%s.%s' %
|
||||
(o.__class__.__module__, o.__class__.__name__))
|
||||
|
||||
|
||||
def pack_byte(b):
|
||||
"""Pack an integer to network-ordered byte"""
|
||||
return struct.pack('!B', b)
|
||||
|
||||
|
||||
class NoopMasker(object):
|
||||
"""A masking object that has the same interface as RepeatedXorMasker but
|
||||
just returns the string passed in without making any change.
|
||||
"""
|
||||
"""A NoOp masking object.
|
||||
|
||||
This has the same interface as RepeatedXorMasker but just returns
|
||||
the string passed in without making any change.
|
||||
"""
|
||||
def __init__(self):
|
||||
"""NoOp."""
|
||||
pass
|
||||
|
||||
def mask(self, s):
|
||||
"""NoOp."""
|
||||
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.
|
||||
"""
|
||||
"""A masking object that applies XOR on the string.
|
||||
|
||||
Applies XOR on the byte string given to mask method with the masking bytes
|
||||
given to the constructor repeatedly. This object remembers the position
|
||||
in the masking bytes the last mask method call ended and resumes from
|
||||
that point on the next mask method call.
|
||||
"""
|
||||
def __init__(self, masking_key):
|
||||
self._masking_key = masking_key
|
||||
self._masking_key_index = 0
|
||||
|
||||
def _mask_using_swig(self, s):
|
||||
masked_data = fast_masking.mask(
|
||||
s, self._masking_key, self._masking_key_index)
|
||||
self._masking_key_index = (
|
||||
(self._masking_key_index + len(s)) % len(self._masking_key))
|
||||
"""Perform the mask via SWIG."""
|
||||
masked_data = fast_masking.mask(s, self._masking_key,
|
||||
self._masking_key_index)
|
||||
self._masking_key_index = ((self._masking_key_index + len(s)) %
|
||||
len(self._masking_key))
|
||||
return masked_data
|
||||
|
||||
def _mask_using_array(self, s):
|
||||
result = array.array('B')
|
||||
result.fromstring(s)
|
||||
"""Perform the mask via python."""
|
||||
if isinstance(s, six.text_type):
|
||||
raise Exception(
|
||||
'Masking Operation should not process unicode strings')
|
||||
|
||||
result = bytearray(s)
|
||||
|
||||
# Use temporary local variables to eliminate the cost to access
|
||||
# attributes
|
||||
masking_key = map(ord, self._masking_key)
|
||||
masking_key = [c for c in six.iterbytes(self._masking_key)]
|
||||
masking_key_size = len(masking_key)
|
||||
masking_key_index = self._masking_key_index
|
||||
|
||||
for i in xrange(len(result)):
|
||||
for i in range(len(result)):
|
||||
result[i] ^= masking_key[masking_key_index]
|
||||
masking_key_index = (masking_key_index + 1) % masking_key_size
|
||||
|
||||
self._masking_key_index = masking_key_index
|
||||
|
||||
return result.tostring()
|
||||
return bytes(result)
|
||||
|
||||
if 'fast_masking' in globals():
|
||||
mask = _mask_using_swig
|
||||
|
@ -224,12 +209,24 @@ class RepeatedXorMasker(object):
|
|||
|
||||
|
||||
class _Deflater(object):
|
||||
|
||||
def __init__(self, window_bits):
|
||||
self._logger = get_class_logger(self)
|
||||
|
||||
self._compress = zlib.compressobj(
|
||||
zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -window_bits)
|
||||
# Using the smallest window bits of 9 for generating input frames.
|
||||
# On WebSocket spec, the smallest window bit is 8. However, zlib does
|
||||
# not accept window_bit = 8.
|
||||
#
|
||||
# Because of a zlib deflate quirk, back-references will not use the
|
||||
# entire range of 1 << window_bits, but will instead use a restricted
|
||||
# range of (1 << window_bits) - 262. With an increased window_bits = 9,
|
||||
# back-references will be within a range of 250. These can still be
|
||||
# decompressed with window_bits = 8 and the 256-byte window used there.
|
||||
#
|
||||
# Similar disscussions can be found in https://crbug.com/691074
|
||||
window_bits = max(window_bits, 9)
|
||||
|
||||
self._compress = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
|
||||
zlib.DEFLATED, -window_bits)
|
||||
|
||||
def compress(self, bytes):
|
||||
compressed_bytes = self._compress.compress(bytes)
|
||||
|
@ -253,12 +250,11 @@ class _Deflater(object):
|
|||
|
||||
|
||||
class _Inflater(object):
|
||||
|
||||
def __init__(self, window_bits):
|
||||
self._logger = get_class_logger(self)
|
||||
self._window_bits = window_bits
|
||||
|
||||
self._unconsumed = ''
|
||||
self._unconsumed = b''
|
||||
|
||||
self.reset()
|
||||
|
||||
|
@ -266,19 +262,12 @@ class _Inflater(object):
|
|||
if not (size == -1 or size > 0):
|
||||
raise Exception('size must be -1 or positive')
|
||||
|
||||
data = ''
|
||||
data = b''
|
||||
|
||||
while True:
|
||||
if size == -1:
|
||||
data += self._decompress.decompress(self._unconsumed)
|
||||
# See Python bug http://bugs.python.org/issue12050 to
|
||||
# understand why the same code cannot be used for updating
|
||||
# self._unconsumed for here and else block.
|
||||
self._unconsumed = ''
|
||||
else:
|
||||
data += self._decompress.decompress(
|
||||
self._unconsumed, size - len(data))
|
||||
self._unconsumed = self._decompress.unconsumed_tail
|
||||
data += self._decompress.decompress(self._unconsumed,
|
||||
max(0, size - len(data)))
|
||||
self._unconsumed = self._decompress.unconsumed_tail
|
||||
if self._decompress.unused_data:
|
||||
# Encountered a last block (i.e. a block with BFINAL = 1) and
|
||||
# found a new stream (unused_data). We cannot use the same
|
||||
|
@ -323,7 +312,6 @@ class _RFC1979Deflater(object):
|
|||
"""A compressor class that applies DEFLATE to given byte sequence and
|
||||
flushes using the algorithm described in the RFC1979 section 2.1.
|
||||
"""
|
||||
|
||||
def __init__(self, window_bits, no_context_takeover):
|
||||
self._deflater = None
|
||||
if window_bits is None:
|
||||
|
@ -338,7 +326,7 @@ class _RFC1979Deflater(object):
|
|||
if bfinal:
|
||||
result = self._deflater.compress_and_finish(bytes)
|
||||
# Add a padding block with BFINAL = 0 and BTYPE = 0.
|
||||
result = result + chr(0)
|
||||
result = result + pack_byte(0)
|
||||
self._deflater = None
|
||||
return result
|
||||
|
||||
|
@ -355,17 +343,18 @@ class _RFC1979Deflater(object):
|
|||
|
||||
|
||||
class _RFC1979Inflater(object):
|
||||
"""A decompressor class for byte sequence compressed and flushed following
|
||||
"""A decompressor class a la RFC1979.
|
||||
|
||||
A decompressor class for byte sequence compressed and flushed following
|
||||
the algorithm described in the RFC1979 section 2.1.
|
||||
"""
|
||||
|
||||
def __init__(self, window_bits=zlib.MAX_WBITS):
|
||||
self._inflater = _Inflater(window_bits)
|
||||
|
||||
def filter(self, bytes):
|
||||
# Restore stripped LEN and NLEN field of a non-compressed block added
|
||||
# for Z_SYNC_FLUSH.
|
||||
self._inflater.append(bytes + '\x00\x00\xff\xff')
|
||||
self._inflater.append(bytes + b'\x00\x00\xff\xff')
|
||||
return self._inflater.decompress(-1)
|
||||
|
||||
|
||||
|
@ -402,7 +391,7 @@ class DeflateSocket(object):
|
|||
|
||||
read_data = self._socket.recv(DeflateSocket._RECV_SIZE)
|
||||
if not read_data:
|
||||
return ''
|
||||
return b''
|
||||
self._inflater.append(read_data)
|
||||
|
||||
def sendall(self, bytes):
|
|
@ -0,0 +1,285 @@
|
|||
# Copyright 2020, 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 WebsocketServer
|
||||
|
||||
This file deals with the main module of standalone server. Although it is fine
|
||||
to import this file directly to use WebSocketServer, it is strongly recommended
|
||||
to use standalone.py, since it is intended to act as a skeleton of this module.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from six.moves import BaseHTTPServer
|
||||
from six.moves import socketserver
|
||||
import logging
|
||||
import re
|
||||
import select
|
||||
import socket
|
||||
import ssl
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from mod_pywebsocket import dispatch
|
||||
from mod_pywebsocket import util
|
||||
from mod_pywebsocket.request_handler import WebSocketRequestHandler
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
with open(websock_handlers_map_file) as f:
|
||||
for line in f:
|
||||
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.DispatchException as e:
|
||||
logging.error(str(e))
|
||||
|
||||
|
||||
class WebSocketServer(socketserver.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
||||
"""HTTPServer specialized for WebSocket."""
|
||||
|
||||
# Overrides SocketServer.ThreadingMixIn.daemon_threads
|
||||
daemon_threads = True
|
||||
# Overrides BaseHTTPServer.HTTPServer.allow_reuse_address
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(self, options):
|
||||
"""Override SocketServer.TCPServer.__init__ to set SSL enabled
|
||||
socket object to self.socket before server_bind and server_activate,
|
||||
if necessary.
|
||||
"""
|
||||
|
||||
# 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,
|
||||
options.allow_handlers_outside_root_dir)
|
||||
if options.websock_handlers_map_file:
|
||||
_alias_handlers(options.dispatcher,
|
||||
options.websock_handlers_map_file)
|
||||
warnings = options.dispatcher.source_warnings()
|
||||
if warnings:
|
||||
for warning in warnings:
|
||||
logging.warning('Warning in source loading: %s' % warning)
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self.request_queue_size = options.request_queue_size
|
||||
self.__ws_is_shut_down = threading.Event()
|
||||
self.__ws_serving = False
|
||||
|
||||
socketserver.BaseServer.__init__(self,
|
||||
(options.server_host, options.port),
|
||||
WebSocketRequestHandler)
|
||||
|
||||
# Expose the options object to allow handler objects access it. We name
|
||||
# it with websocket_ prefix to avoid conflict.
|
||||
self.websocket_server_options = options
|
||||
|
||||
self._create_sockets()
|
||||
self.server_bind()
|
||||
self.server_activate()
|
||||
|
||||
def _create_sockets(self):
|
||||
self.server_name, self.server_port = self.server_address
|
||||
self._sockets = []
|
||||
if not self.server_name:
|
||||
# On platforms that doesn't support IPv6, the first bind fails.
|
||||
# On platforms that supports IPv6
|
||||
# - If it binds both IPv4 and IPv6 on call with AF_INET6, the
|
||||
# first bind succeeds and the second fails (we'll see 'Address
|
||||
# already in use' error).
|
||||
# - If it binds only IPv6 on call with AF_INET6, both call are
|
||||
# expected to succeed to listen both protocol.
|
||||
addrinfo_array = [(socket.AF_INET6, socket.SOCK_STREAM, '', '',
|
||||
''),
|
||||
(socket.AF_INET, socket.SOCK_STREAM, '', '', '')]
|
||||
else:
|
||||
addrinfo_array = socket.getaddrinfo(self.server_name,
|
||||
self.server_port,
|
||||
socket.AF_UNSPEC,
|
||||
socket.SOCK_STREAM,
|
||||
socket.IPPROTO_TCP)
|
||||
for addrinfo in addrinfo_array:
|
||||
self._logger.info('Create socket on: %r', addrinfo)
|
||||
family, socktype, proto, canonname, sockaddr = addrinfo
|
||||
try:
|
||||
socket_ = socket.socket(family, socktype)
|
||||
except Exception as e:
|
||||
self._logger.info('Skip by failure: %r', e)
|
||||
continue
|
||||
server_options = self.websocket_server_options
|
||||
if server_options.use_tls:
|
||||
if server_options.tls_client_auth:
|
||||
if server_options.tls_client_cert_optional:
|
||||
client_cert_ = ssl.CERT_OPTIONAL
|
||||
else:
|
||||
client_cert_ = ssl.CERT_REQUIRED
|
||||
else:
|
||||
client_cert_ = ssl.CERT_NONE
|
||||
socket_ = ssl.wrap_socket(
|
||||
socket_,
|
||||
keyfile=server_options.private_key,
|
||||
certfile=server_options.certificate,
|
||||
ca_certs=server_options.tls_client_ca,
|
||||
cert_reqs=client_cert_)
|
||||
self._sockets.append((socket_, addrinfo))
|
||||
|
||||
def server_bind(self):
|
||||
"""Override SocketServer.TCPServer.server_bind to enable multiple
|
||||
sockets bind.
|
||||
"""
|
||||
|
||||
failed_sockets = []
|
||||
|
||||
for socketinfo in self._sockets:
|
||||
socket_, addrinfo = socketinfo
|
||||
self._logger.info('Bind on: %r', addrinfo)
|
||||
if self.allow_reuse_address:
|
||||
socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
socket_.bind(self.server_address)
|
||||
except Exception as e:
|
||||
self._logger.info('Skip by failure: %r', e)
|
||||
socket_.close()
|
||||
failed_sockets.append(socketinfo)
|
||||
if self.server_address[1] == 0:
|
||||
# The operating system assigns the actual port number for port
|
||||
# number 0. This case, the second and later sockets should use
|
||||
# the same port number. Also self.server_port is rewritten
|
||||
# because it is exported, and will be used by external code.
|
||||
self.server_address = (self.server_name,
|
||||
socket_.getsockname()[1])
|
||||
self.server_port = self.server_address[1]
|
||||
self._logger.info('Port %r is assigned', self.server_port)
|
||||
|
||||
for socketinfo in failed_sockets:
|
||||
self._sockets.remove(socketinfo)
|
||||
|
||||
def server_activate(self):
|
||||
"""Override SocketServer.TCPServer.server_activate to enable multiple
|
||||
sockets listen.
|
||||
"""
|
||||
|
||||
failed_sockets = []
|
||||
|
||||
for socketinfo in self._sockets:
|
||||
socket_, addrinfo = socketinfo
|
||||
self._logger.info('Listen on: %r', addrinfo)
|
||||
try:
|
||||
socket_.listen(self.request_queue_size)
|
||||
except Exception as e:
|
||||
self._logger.info('Skip by failure: %r', e)
|
||||
socket_.close()
|
||||
failed_sockets.append(socketinfo)
|
||||
|
||||
for socketinfo in failed_sockets:
|
||||
self._sockets.remove(socketinfo)
|
||||
|
||||
if len(self._sockets) == 0:
|
||||
self._logger.critical(
|
||||
'No sockets activated. Use info log level to see the reason.')
|
||||
|
||||
def server_close(self):
|
||||
"""Override SocketServer.TCPServer.server_close to enable multiple
|
||||
sockets close.
|
||||
"""
|
||||
|
||||
for socketinfo in self._sockets:
|
||||
socket_, addrinfo = socketinfo
|
||||
self._logger.info('Close on: %r', addrinfo)
|
||||
socket_.close()
|
||||
|
||||
def fileno(self):
|
||||
"""Override SocketServer.TCPServer.fileno."""
|
||||
|
||||
self._logger.critical('Not supported: fileno')
|
||||
return self._sockets[0][0].fileno()
|
||||
|
||||
def handle_error(self, request, client_address):
|
||||
"""Override SocketServer.handle_error."""
|
||||
|
||||
self._logger.error('Exception in processing request from: %r\n%s',
|
||||
client_address, traceback.format_exc())
|
||||
# Note: client_address is a tuple.
|
||||
|
||||
def get_request(self):
|
||||
"""Override TCPServer.get_request."""
|
||||
|
||||
accepted_socket, client_address = self.socket.accept()
|
||||
|
||||
server_options = self.websocket_server_options
|
||||
if server_options.use_tls:
|
||||
# Print cipher in use. Handshake is done on accept.
|
||||
self._logger.debug('Cipher: %s', accepted_socket.cipher())
|
||||
self._logger.debug('Client cert: %r',
|
||||
accepted_socket.getpeercert())
|
||||
|
||||
return accepted_socket, client_address
|
||||
|
||||
def serve_forever(self, poll_interval=0.5):
|
||||
"""Override SocketServer.BaseServer.serve_forever."""
|
||||
|
||||
self.__ws_serving = True
|
||||
self.__ws_is_shut_down.clear()
|
||||
handle_request = self.handle_request
|
||||
if hasattr(self, '_handle_request_noblock'):
|
||||
handle_request = self._handle_request_noblock
|
||||
else:
|
||||
self._logger.warning('Fallback to blocking request handler')
|
||||
try:
|
||||
while self.__ws_serving:
|
||||
r, w, e = select.select(
|
||||
[socket_[0] for socket_ in self._sockets], [], [],
|
||||
poll_interval)
|
||||
for socket_ in r:
|
||||
self.socket = socket_
|
||||
handle_request()
|
||||
self.socket = None
|
||||
finally:
|
||||
self.__ws_is_shut_down.set()
|
||||
|
||||
def shutdown(self):
|
||||
"""Override SocketServer.BaseServer.shutdown."""
|
||||
|
||||
self.__ws_serving = False
|
||||
self.__ws_is_shut_down.wait()
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
|
@ -13,8 +13,8 @@ import signal
|
|||
import sys
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.path = ['pywebsocket'] + sys.path
|
||||
import standalone
|
||||
sys.path = ['pywebsocket3'] + sys.path
|
||||
from mod_pywebsocket import standalone
|
||||
|
||||
# If we received --interactive as the first argument, ignore SIGINT so
|
||||
# pywebsocket doesn't die on a ctrl+c meant for the debugger. Otherwise,
|
||||
|
|
|
@ -147,7 +147,7 @@ security/sandbox/chromium-shim/
|
|||
testing/gtest/gmock/
|
||||
testing/gtest/gtest/
|
||||
testing/mochitest/MochiKit/
|
||||
testing/mochitest/pywebsocket
|
||||
testing/mochitest/pywebsocket3/
|
||||
testing/mochitest/tests/MochiKit-1.4.2/
|
||||
testing/modules/ajv-4.1.1.js
|
||||
testing/modules/sinon-7.2.7.js
|
||||
|
|
Загрузка…
Ссылка в новой задаче