From 2a58edc446c39f63f422e85b9433b3c091c67c9e Mon Sep 17 00:00:00 2001 From: Jason Duell Date: Tue, 20 Dec 2011 00:20:00 -0800 Subject: [PATCH] Bug 710345: Upgrade pywebsocket to v606 (RFC 6455). r=mcmanus --- .../pywebsocket/mod_pywebsocket/__init__.py | 6 + .../mod_pywebsocket/_stream_base.py | 11 +- .../mod_pywebsocket/_stream_hybi.py | 21 +- .../pywebsocket/mod_pywebsocket/common.py | 20 +- .../pywebsocket/mod_pywebsocket/dispatch.py | 67 +++-- .../pywebsocket/mod_pywebsocket/extensions.py | 4 + .../mod_pywebsocket/handshake/__init__.py | 20 +- .../mod_pywebsocket/handshake/_base.py | 32 ++- .../mod_pywebsocket/handshake/hybi.py | 68 ++++-- .../mod_pywebsocket/handshake/hybi00.py | 8 +- .../mod_pywebsocket/headerparserhandler.py | 134 +++++++--- .../mod_pywebsocket/http_header_util.py | 11 +- .../pywebsocket/mod_pywebsocket/util.py | 6 + testing/mochitest/pywebsocket/standalone.py | 231 ++++++++++++------ 14 files changed, 464 insertions(+), 175 deletions(-) diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/__init__.py b/testing/mochitest/pywebsocket/mod_pywebsocket/__init__.py index 2171e5a29746..c154da4a140e 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/__init__.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/__init__.py @@ -65,6 +65,12 @@ Installation: PythonOption mod_pywebsocket.allow_draft75 On + 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.) diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_base.py b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_base.py index 35787095f000..60fb33d2c5c0 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_base.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_base.py @@ -111,14 +111,9 @@ class StreamBase(object): bytes = self._request.connection.read(length) if not bytes: - # MOZILLA: Patrick McManus found we needed this for Python 2.5 to - # work. Not sure which tests he meant: I found that - # content/base/test/test_websocket* all worked fine with 2.5 with - # the original Google code. JDuell - #raise ConnectionTerminatedException( - # 'Receiving %d byte failed. Peer (%r) closed connection' % - # (length, (self._request.connection.remote_addr,))) - raise ConnectionTerminatedException('connection terminated: read failed') + raise ConnectionTerminatedException( + 'Receiving %d byte failed. Peer (%r) closed connection' % + (length, (self._request.connection.remote_addr,))) return bytes def _write(self, bytes): diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hybi.py b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hybi.py index 0ba6e0a6ab32..c850a67dd7a8 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hybi.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hybi.py @@ -298,9 +298,11 @@ class Stream(StreamBase): 'Mask bit on the received frame did\'nt match masking ' 'configuration for received frames') - # The spec doesn't disallow putting a value in 0x0-0xFFFF into the - # 8-octet extended payload length field (or 0x0-0xFD in 2-octet field). - # So, we don't check the range of extended_payload_length. + # The Hybi-13 and later specs disallow putting a value in 0x0-0xFFFF + # into the 8-octet extended payload length field (or 0x0-0xFD in + # 2-octet field). + valid_length_encoding = True + length_encoding_bytes = 1 if payload_length == 127: extended_payload_length = self.receive_bytes(8) payload_length = struct.unpack( @@ -308,10 +310,23 @@ class Stream(StreamBase): if payload_length > 0x7FFFFFFFFFFFFFFF: raise InvalidFrameException( 'Extended payload length >= 2^63') + if self._request.ws_version >= 13 and payload_length < 0x10000: + valid_length_encoding = False + length_encoding_bytes = 8 elif payload_length == 126: extended_payload_length = self.receive_bytes(2) payload_length = struct.unpack( '!H', extended_payload_length)[0] + if self._request.ws_version >= 13 and payload_length < 126: + valid_length_encoding = False + length_encoding_bytes = 2 + + if not valid_length_encoding: + self._logger.warning( + 'Payload length is not encoded using the minimal number of ' + 'bytes (%d is encoded using %d bytes)', + payload_length, + length_encoding_bytes) if mask == 1: masking_nonce = self.receive_bytes(4) diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/common.py b/testing/mochitest/pywebsocket/mod_pywebsocket/common.py index 36097f454368..bb5c40ea8838 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/common.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/common.py @@ -41,9 +41,16 @@ 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 +VERSION_HYBI16 = 13 +VERSION_HYBI17 = 13 # Constants indicating WebSocket protocol latest version. -VERSION_HYBI_LATEST = VERSION_HYBI10 +VERSION_HYBI_LATEST = VERSION_HYBI13 # Port numbers DEFAULT_WEB_SOCKET_PORT = 80 @@ -95,10 +102,17 @@ STATUS_NORMAL = 1000 STATUS_GOING_AWAY = 1001 STATUS_PROTOCOL_ERROR = 1002 STATUS_UNSUPPORTED = 1003 -STATUS_TOO_LARGE = 1004 STATUS_CODE_NOT_AVAILABLE = 1005 STATUS_ABNORMAL_CLOSE = 1006 -STATUS_INVALID_UTF8 = 1007 +STATUS_INVALID_FRAME_PAYLOAD = 1007 +STATUS_POLICY_VIOLATION = 1008 +STATUS_MESSAGE_TOO_BIG = 1009 +STATUS_MANDATORY_EXT = 1010 + +# HTTP status codes +HTTP_STATUS_BAD_REQUEST = 400 +HTTP_STATUS_FORBIDDEN = 403 +HTTP_STATUS_NOT_FOUND = 404 def is_control_opcode(opcode): diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py b/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py index f13dbf676606..ad4e6ac63ed6 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py @@ -54,13 +54,14 @@ _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME = ( class DispatchException(Exception): """Exception in dispatching WebSocket request.""" - def __init__(self, name, status=404): + def __init__(self, name, status=common.HTTP_STATUS_NOT_FOUND): super(DispatchException, self).__init__(name) self.status = status def _default_passive_closing_handshake_handler(request): """Default web_socket_passive_closing_handshake handler.""" + return common.STATUS_NORMAL, '' @@ -76,15 +77,21 @@ def _normalize_path(path): """ path = path.replace('\\', os.path.sep) - - # MOZILLA: do not normalize away symlinks in mochitest - #path = os.path.realpath(path) - + path = os.path.realpath(path) path = path.replace('\\', '/') return path def _create_path_to_resource_converter(base_dir): + """Returns a function that converts the path of a WebSocket handler source + file to a resource string by removing the path to the base directory from + its head, removing _SOURCE_SUFFIX from its tail, and replacing path + separators in it with '/'. + + Args: + base_dir: the path to the base directory. + """ + base_dir = _normalize_path(base_dir) base_len = len(base_dir) @@ -93,7 +100,9 @@ def _create_path_to_resource_converter(base_dir): def converter(path): if not path.endswith(_SOURCE_SUFFIX): return None - path = _normalize_path(path) + # _normalize_path must not be used because resolving symlink breaks + # following path check. + path = path.replace('\\', '/') if not path.startswith(base_dir): return None return path[base_len:-suffix_len] @@ -169,7 +178,9 @@ class Dispatcher(object): This class maintains a map from resource name to handlers. """ - def __init__(self, root_dir, scan_dir=None): + def __init__( + self, root_dir, scan_dir=None, + allow_handlers_outside_root_dir=True): """Construct an instance. Args: @@ -181,6 +192,8 @@ class Dispatcher(object): root_dir is used as scan_dir. scan_dir can be useful in saving scan time when root_dir contains many subdirectories. + allow_handlers_outside_root_dir: Scans handler files even if their + canonical path is not under root_dir. """ self._logger = util.get_class_logger(self) @@ -193,7 +206,8 @@ 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) + 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): @@ -247,7 +261,7 @@ class Dispatcher(object): _DO_EXTRA_HANDSHAKE_HANDLER_NAME, request.ws_resource), e) - raise handshake.HandshakeException(e, 403) + raise handshake.HandshakeException(e, common.HTTP_STATUS_FORBIDDEN) def transfer_data(self, request): """Let a handler transfer_data with a WebSocket client. @@ -288,8 +302,9 @@ class Dispatcher(object): self._logger.debug('%s', e) request.ws_stream.close_connection(common.STATUS_UNSUPPORTED) except stream.InvalidUTF8Exception, e: - self._logger_debug('%s', e) - request.ws_stream.close_connection(common.STATUS_INVALID_UTF8) + self._logger.debug('%s', e) + request.ws_stream.close_connection( + common.STATUS_INVALID_FRAME_PAYLOAD) except msgutil.ConnectionTerminatedException, e: self._logger.debug('%s', e) except Exception, e: @@ -322,23 +337,45 @@ class Dispatcher(object): handler_suite = self._handler_suite_map.get(resource) if handler_suite and fragment: raise DispatchException('Fragment identifiers MUST NOT be used on ' - 'WebSocket URIs', 400); + 'WebSocket URIs', + common.HTTP_STATUS_BAD_REQUEST) return handler_suite - def _source_handler_files_in_dir(self, root_dir, scan_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. """ + # We build a map from resource to handler code assuming that there's + # only one path from root_dir to scan_dir and it can be obtained by + # comparing realpath of them. + + # Here we cannot use abspath. See + # https://bugs.webkit.org/show_bug.cgi?id=31603 + convert = _create_path_to_resource_converter(root_dir) - for path in _enumerate_handler_file_paths(scan_dir): + scan_realpath = os.path.realpath(scan_dir) + root_realpath = os.path.realpath(root_dir) + for path in _enumerate_handler_file_paths(scan_realpath): + 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) + continue try: handler_suite = _source_handler_file(open(path).read()) except DispatchException, e: self._source_warnings.append('%s: %s' % (path, e)) continue - self._handler_suite_map[convert(path)] = handler_suite + resource = convert(path) + if resource is None: + self._logger.debug( + 'Path to resource conversion on %s failed' % path) + else: + self._handler_suite_map[convert(path)] = handler_suite # vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/extensions.py b/testing/mochitest/pywebsocket/mod_pywebsocket/extensions.py index 031829d84d79..562ad6f12c00 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/extensions.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/extensions.py @@ -36,6 +36,7 @@ _available_processors = {} class ExtensionProcessorInterface(object): + def get_extension_response(self): return None @@ -131,7 +132,9 @@ class DeflateFrameExtensionProcessor(ExtensionProcessorInterface): return response def setup_stream_options(self, stream_options): + class _OutgoingFilter(object): + def __init__(self, parent): self._parent = parent @@ -139,6 +142,7 @@ class DeflateFrameExtensionProcessor(ExtensionProcessorInterface): self._parent._outgoing_filter(frame) class _IncomingFilter(object): + def __init__(self, parent): self._parent = parent diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/__init__.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/__init__.py index 622707e73c6b..93659be3a216 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/__init__.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/__init__.py @@ -36,12 +36,15 @@ successfully established. import logging +from mod_pywebsocket import common from mod_pywebsocket.handshake import draft75 from mod_pywebsocket.handshake import hybi00 from mod_pywebsocket.handshake import hybi -# Export AbortedByUserException and HandshakeException symbol from this module. +# Export AbortedByUserException, HandshakeException, and VersionException +# symbol from this module. 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__) @@ -62,7 +65,7 @@ def do_handshake(request, dispatcher, allowDraft75=False, strict=False): handshake. """ - _LOGGER.debug('Opening handshake resource: %r', request.uri) + _LOGGER.debug('Client\'s opening handshake resource: %r', request.uri) # To print mimetools.Message as escaped one-line string, we converts # headers_in to dict object. Without conversion, if we use %r, it just # prints the type and address, and if we use %s, it prints the original @@ -76,7 +79,7 @@ def do_handshake(request, dispatcher, allowDraft75=False, strict=False): # header values. While MpTable_Type doesn't have such __str__ but just # __repr__ which formats itself as well as dictionary object. _LOGGER.debug( - 'Opening handshake request headers: %r', dict(request.headers_in)) + 'Client\'s opening handshake headers: %r', dict(request.headers_in)) handshakers = [] handshakers.append( @@ -88,21 +91,26 @@ def do_handshake(request, dispatcher, allowDraft75=False, strict=False): ('IETF Hixie 75', draft75.Handshaker(request, dispatcher, strict))) for name, handshaker in handshakers: - _LOGGER.info('Trying %s protocol', name) + _LOGGER.debug('Trying %s protocol', name) try: handshaker.do_handshake() + _LOGGER.info('Established (%s protocol)', name) return except HandshakeException, e: - _LOGGER.info( + _LOGGER.debug( 'Failed to complete opening handshake as %s protocol: %r', name, e) if e.status: raise e except AbortedByUserException, e: raise + except VersionException, e: + raise + # TODO(toyoshim): Add a test to cover the case all handshakers fail. raise HandshakeException( - 'Failed to complete opening handshake for all available protocols') + 'Failed to complete opening handshake for all available protocols', + status=common.HTTP_STATUS_BAD_REQUEST) # vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/_base.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/_base.py index b67305f8373f..4c179187b952 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/_base.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/_base.py @@ -61,6 +61,22 @@ class HandshakeException(Exception): self.status = status +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') + """ + super(VersionException, self).__init__(name) + self.supported_versions = supported_versions + + def get_default_port(is_secure): if is_secure: return common.DEFAULT_WEB_SOCKET_SECURE_PORT @@ -200,7 +216,7 @@ def parse_token_list(data): return token_list -def _parse_extension_param(state, definition): +def _parse_extension_param(state, definition, allow_quoted_string): param_name = http_header_util.consume_token(state) if param_name is None: @@ -214,7 +230,11 @@ def _parse_extension_param(state, definition): http_header_util.consume_lwses(state) - param_value = http_header_util.consume_token_or_quoted_string(state) + if allow_quoted_string: + # TODO(toyoshim): Add code to validate that parsed param_value is token + param_value = http_header_util.consume_token_or_quoted_string(state) + else: + param_value = http_header_util.consume_token(state) if param_value is None: raise HandshakeException( 'No valid parameter value found on the right-hand side of ' @@ -223,7 +243,7 @@ def _parse_extension_param(state, definition): definition.add_parameter(param_name, param_value) -def _parse_extension(state): +def _parse_extension(state, allow_quoted_string): extension_token = http_header_util.consume_token(state) if extension_token is None: return None @@ -239,7 +259,7 @@ def _parse_extension(state): http_header_util.consume_lwses(state) try: - _parse_extension_param(state, extension) + _parse_extension_param(state, extension, allow_quoted_string) except HandshakeException, e: raise HandshakeException( 'Failed to parse Sec-WebSocket-Extensions header: ' @@ -249,7 +269,7 @@ def _parse_extension(state): return extension -def parse_extensions(data): +def parse_extensions(data, allow_quoted_string=False): """Parses Sec-WebSocket-Extensions header value returns a list of common.ExtensionParameter objects. @@ -260,7 +280,7 @@ def parse_extensions(data): extension_list = [] while True: - extension = _parse_extension(state) + extension = _parse_extension(state, allow_quoted_string) if extension is not None: extension_list.append(extension) diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py index be2c2f3cd9ae..67ec4ffe488b 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py @@ -53,6 +53,7 @@ from mod_pywebsocket.handshake._base import parse_extensions from mod_pywebsocket.handshake._base import parse_token_list from mod_pywebsocket.handshake._base import validate_mandatory_header from mod_pywebsocket.handshake._base import validate_subprotocol +from mod_pywebsocket.handshake._base import VersionException from mod_pywebsocket.stream import Stream from mod_pywebsocket.stream import StreamOptions from mod_pywebsocket import util @@ -60,6 +61,16 @@ from mod_pywebsocket import util _BASE64_REGEX = re.compile('^[+/0-9A-Za-z]*=*$') +# Defining aliases for values used frequently. +_VERSION_HYBI08 = common.VERSION_HYBI08 +_VERSION_HYBI08_STRING = str(_VERSION_HYBI08) +_VERSION_LATEST = common.VERSION_HYBI_LATEST +_VERSION_LATEST_STRING = str(_VERSION_LATEST) +_SUPPORTED_VERSIONS = [ + _VERSION_LATEST, + _VERSION_HYBI08, +] + def compute_accept(key): """Computes value for the Sec-WebSocket-Accept header from value of the @@ -130,7 +141,7 @@ class Handshaker(object): unused_host = get_mandatory_header(self._request, common.HOST_HEADER) - self._check_version() + self._request.ws_version = self._check_version() # This handshake must be based on latest hybi. We are responsible to # fallback to HTTP on handshake failure as latest hybi handshake @@ -151,7 +162,6 @@ class Handshaker(object): util.hexify(accept_binary)) self._logger.debug('IETF HyBi protocol') - self._request.ws_version = common.VERSION_HYBI_LATEST # Setup extension processors. @@ -212,29 +222,42 @@ class Handshaker(object): 'request any subprotocol') self._send_handshake(accept) - - self._logger.debug('Sent opening handshake response') except HandshakeException, e: if not e.status: # Fallback to 400 bad request by default. - e.status = 400 + e.status = common.HTTP_STATUS_BAD_REQUEST raise e def _get_origin(self): - origin = self._request.headers_in.get( - common.SEC_WEBSOCKET_ORIGIN_HEADER) + if self._request.ws_version is _VERSION_HYBI08: + origin_header = common.SEC_WEBSOCKET_ORIGIN_HEADER + else: + origin_header = common.ORIGIN_HEADER + origin = self._request.headers_in.get(origin_header) + if origin is None: + self._logger.debug('Client request does not have origin header') self._request.ws_origin = origin def _check_version(self): - unused_value = validate_mandatory_header( - self._request, common.SEC_WEBSOCKET_VERSION_HEADER, - str(common.VERSION_HYBI_LATEST), fail_status=426) + version = get_mandatory_header(self._request, + common.SEC_WEBSOCKET_VERSION_HEADER) + if version == _VERSION_HYBI08_STRING: + return _VERSION_HYBI08 + if version == _VERSION_LATEST_STRING: + return _VERSION_LATEST + + if version.find(',') >= 0: + raise HandshakeException( + '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))) 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) @@ -255,8 +278,12 @@ class Handshaker(object): self._request.ws_requested_extensions = None return + if self._request.ws_version is common.VERSION_HYBI08: + allow_quoted_string=False + else: + allow_quoted_string=True self._request.ws_requested_extensions = parse_extensions( - extensions_header) + extensions_header, allow_quoted_string=allow_quoted_string) self._logger.debug( 'Extensions requested: %r', @@ -264,6 +291,11 @@ class Handshaker(object): self._request.ws_requested_extensions)) def _validate_key(self, key): + if key.find(',') >= 0: + raise HandshakeException('Request has multiple %s header lines or ' + 'contains illegal character \',\': %r' % + (common.SEC_WEBSOCKET_KEY_HEADER, key)) + # Validate key_is_valid = False try: @@ -319,16 +351,12 @@ class Handshaker(object): response.append(format_header( common.SEC_WEBSOCKET_EXTENSIONS_HEADER, 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 response.append('\r\n') raw_response = ''.join(response) - self._logger.debug('Opening handshake response: %r', raw_response) self._request.connection.write(raw_response) + self._logger.debug('Sent server\'s opening handshake: %r', + raw_response) # vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi00.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi00.py index 776eb85d22b9..81b05bfec603 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi00.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi00.py @@ -107,8 +107,6 @@ class Handshaker(object): self._send_handshake() - self._logger.debug('Sent opening handshake response') - def _set_resource(self): self._request.ws_resource = self._request.uri @@ -138,7 +136,8 @@ class Handshaker(object): 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)) + (common.SEC_WEBSOCKET_DRAFT_HEADER, + draft)) self._logger.debug('IETF HyBi 00 protocol') self._request.ws_version = common.VERSION_HYBI00 @@ -229,8 +228,9 @@ class Handshaker(object): response.append(self._request.ws_challenge_md5) raw_response = ''.join(response) - self._logger.debug('Opening handshake response: %r', raw_response) self._request.connection.write(raw_response) + self._logger.debug('Sent server\'s opening handshake: %r', + raw_response) # vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/headerparserhandler.py b/testing/mochitest/pywebsocket/mod_pywebsocket/headerparserhandler.py index a9e5a2f60886..b68c240e165e 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/headerparserhandler.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/headerparserhandler.py @@ -39,6 +39,7 @@ 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 @@ -52,9 +53,21 @@ _PYOPT_HANDLER_ROOT = 'mod_pywebsocket.handler_root' # 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} + # PythonOption to specify to allow draft75 handshake. # 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): @@ -70,15 +83,20 @@ class ApacheLogHandler(logging.Handler): def __init__(self, request=None): logging.Handler.__init__(self) - self.log_error = apache.log_error + self._log_error = apache.log_error if request is not None: - self.log_error = request.log_error + 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 @@ -99,28 +117,57 @@ class ApacheLogHandler(logging.Handler): # 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(record.getMessage(), apache_level, apache.main_server) + self._log_error(msg, apache_level, apache.main_server) -_LOGGER = logging.getLogger('mod_pywebsocket') -# 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()) +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(): - _HANDLER_ROOT = apache.main_server.get_options().get( - _PYOPT_HANDLER_ROOT, None) - if not _HANDLER_ROOT: + _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 = apache.main_server.get_options().get( - _PYOPT_HANDLER_SCAN, _HANDLER_ROOT) - dispatcher = dispatch.Dispatcher(_HANDLER_ROOT, _HANDLER_SCAN) + + 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: %s' % warning, apache.APLOG_WARNING) + return dispatcher @@ -140,33 +187,54 @@ def headerparserhandler(request): handshake_is_done = False try: - allowDraft75 = apache.main_server.get_options().get( - _PYOPT_ALLOW_DRAFT75, None) - handshake.do_handshake( - request, _dispatcher, allowDraft75=allowDraft75) - handshake_is_done = True - request.log_error( - 'mod_pywebsocket: resource: %r' % request.ws_resource, - apache.APLOG_DEBUG) - request._dispatcher = _dispatcher - _dispatcher.transfer_data(request) + # 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('No handler for resource: %r' % request.uri, + apache.APLOG_INFO) + request.log_error('Fallback to Apache', apache.APLOG_INFO) + return apache.DECLINED except dispatch.DispatchException, e: - request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_WARNING) + request.log_error('mod_pywebsocket: %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: %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: %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: %s' % e, apache.APLOG_INFO) - except handshake.HandshakeException, e: - # Handshake for ws/wss failed. - # The request handling fallback into http/https. - request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_INFO) - return e.status except Exception, e: - request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_WARNING) + # DispatchException can also be thrown if something is wrong in + # pywebsocket code. It's caught here, then. + + request.log_error('mod_pywebsocket: %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.DECLINE + 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. diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/http_header_util.py b/testing/mochitest/pywebsocket/mod_pywebsocket/http_header_util.py index 383c9801ee0d..b77465393285 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/http_header_util.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/http_header_util.py @@ -52,6 +52,7 @@ def _is_ctl(c): class ParsingState(object): + def __init__(self, data): self.data = data self.head = 0 @@ -209,7 +210,7 @@ def quote_if_necessary(s): result.append(c) if quote: - return '"' + ''.join(result) + '"'; + return '"' + ''.join(result) + '"' else: return ''.join(result) @@ -251,4 +252,12 @@ 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 diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/util.py b/testing/mochitest/pywebsocket/mod_pywebsocket/util.py index 6cc1be559fd3..9a0ab5de697a 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/util.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/util.py @@ -218,6 +218,7 @@ class DeflateRequest(object): class _Deflater(object): + def __init__(self, window_bits): self._logger = get_class_logger(self) @@ -233,6 +234,7 @@ class _Deflater(object): class _Inflater(object): + def __init__(self): self._logger = get_class_logger(self) @@ -390,6 +392,10 @@ class DeflateConnection(object): self._deflater = _Deflater(zlib.MAX_WBITS) self._inflater = _Inflater() + def get_remote_addr(self): + return self._connection.remote_addr + remote_addr = property(get_remote_addr) + def put_bytes(self, bytes): self.write(bytes) diff --git a/testing/mochitest/pywebsocket/standalone.py b/testing/mochitest/pywebsocket/standalone.py index 49d2b09fdf13..5a2e96f1b3d5 100755 --- a/testing/mochitest/pywebsocket/standalone.py +++ b/testing/mochitest/pywebsocket/standalone.py @@ -65,6 +65,7 @@ import BaseHTTPServer import CGIHTTPServer import SimpleHTTPServer import SocketServer +import httplib import logging import logging.handlers import optparse @@ -74,6 +75,8 @@ import select import socket import sys import threading +import time + _HAS_OPEN_SSL = False try: @@ -99,13 +102,6 @@ _DEFAULT_REQUEST_QUEUE_SIZE = 128 _MAX_MEMORIZED_LINES = 1024 -def _print_warnings_if_any(dispatcher): - warnings = dispatcher.source_warnings() - if warnings: - for warning in warnings: - logging.warning('mod_pywebsocket: %s' % warning) - - class _StandaloneConnection(object): """Mimic mod_python mp_conn.""" @@ -165,6 +161,7 @@ class _StandaloneRequest(object): 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.""" @@ -178,12 +175,6 @@ class _StandaloneRequest(object): return self._request_handler.command method = property(get_method) - def get_headers_in(self): - """Getter to mimic request.headers_in.""" - - return self._request_handler.headers - headers_in = property(get_headers_in) - def is_https(self): """Mimic request.is_https().""" @@ -216,6 +207,8 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): if necessary. """ + 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 @@ -235,8 +228,16 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): 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 = [ - (self.address_family, self.socket_type, '', '', '')] + (socket.AF_INET6, socket.SOCK_STREAM, '', '', ''), + (socket.AF_INET, socket.SOCK_STREAM, '', '', '')] else: addrinfo_array = socket.getaddrinfo(self.server_name, self.server_port, @@ -244,12 +245,12 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): socket.SOCK_STREAM, socket.IPPROTO_TCP) for addrinfo in addrinfo_array: - logging.info('Create socket on: %r', addrinfo) + self._logger.info('Create socket on: %r', addrinfo) family, socktype, proto, canonname, sockaddr = addrinfo try: socket_ = socket.socket(family, socktype) except Exception, e: - logging.info('Skip by failure: %r', e) + self._logger.info('Skip by failure: %r', e) continue if self.websocket_server_options.use_tls: ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) @@ -265,11 +266,22 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): sockets bind. """ - for socket_, addrinfo in self._sockets: - logging.info('Bind on: %r', addrinfo) + 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) - socket_.bind(self.server_address) + try: + socket_.bind(self.server_address) + except Exception, e: + self._logger.info('Skip by failure: %r', e) + socket_.close() + failed_sockets.append(socketinfo) + + for socketinfo in failed_sockets: + self._sockets.remove(socketinfo) def server_activate(self): """Override SocketServer.TCPServer.server_activate to enable multiple @@ -280,11 +292,11 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): for socketinfo in self._sockets: socket_, addrinfo = socketinfo - logging.info('Listen on: %r', addrinfo) + self._logger.info('Listen on: %r', addrinfo) try: socket_.listen(self.request_queue_size) except Exception, e: - logging.info('Skip by failure: %r', e) + self._logger.info('Skip by failure: %r', e) socket_.close() failed_sockets.append(socketinfo) @@ -298,23 +310,23 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): for socketinfo in self._sockets: socket_, addrinfo = socketinfo - logging.info('Close on: %r', addrinfo) + self._logger.info('Close on: %r', addrinfo) socket_.close() def fileno(self): """Override SocketServer.TCPServer.fileno.""" - logging.critical('Not supported: fileno') + self._logger.critical('Not supported: fileno') return self._sockets[0][0].fileno() def handle_error(self, rquest, client_address): """Override SocketServer.handle_error.""" - logging.error( - ('Exception in processing request from: %r' % (client_address,)) + - '\n' + util.get_stack_trace()) - # Note: client_address is a tuple. To match it against %r, we need the - # trailing comma. + self._logger.error( + 'Exception in processing request from: %r\n%s', + client_address, + util.get_stack_trace()) + # Note: client_address is a tuple. def serve_forever(self, poll_interval=0.5): """Override SocketServer.BaseServer.serve_forever.""" @@ -325,8 +337,7 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): if hasattr(self, '_handle_request_noblock'): handle_request = self._handle_request_noblock else: - logging.warning('mod_pywebsocket: fallback to blocking request ' - 'handler') + self._logger.warning('Fallback to blocking request handler') try: while self.__ws_serving: r, w, e = select.select( @@ -349,6 +360,9 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): """CGIHTTPRequestHandler specialized for WebSocket.""" + # Use httplib.HTTPMessage instead of mimetools.Message. + MessageClass = httplib.HTTPMessage + def setup(self): """Override SocketServer.StreamRequestHandler.setup to wrap rfile with MemorizingFile. @@ -370,6 +384,8 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): 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. @@ -378,10 +394,6 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): if self._options.is_executable_method is not None: self.is_executable = self._options.is_executable_method - self._request = _StandaloneRequest(self, self._options.use_tls) - - _print_warnings_if_any(self._options.dispatcher) - # This actually calls BaseRequestHandler.__init__. CGIHTTPServer.CGIHTTPRequestHandler.__init__( self, request, client_address, server) @@ -406,79 +418,87 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): return False host, port, resource = http_header_util.parse_uri(self.path) if resource is None: - logging.info('mod_pywebsocket: invalid uri %r' % self.path) + 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: - logging.info('mod_pywebsocket: invalid host %r ' - '(expected: %r)' % (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: - logging.info('mod_pywebsocket: invalid port %r ' - '(expected: %r)' % (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): - logging.info('No handlers for request: %s' % self.path) + self._logger.info('No handler for resource: %r', + self.path) + self._logger.info('Fallback to CGIHTTPRequestHandler') return True + except dispatch.DispatchException, e: + self._logger.info('%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( - self._request, + request, self._options.dispatcher, allowDraft75=self._options.allow_draft75, strict=self._options.strict) - except handshake.AbortedByUserException, e: - logging.info('mod_pywebsocket: %s' % e) + except handshake.VersionException, e: + self._logger.info('%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 - try: - self._request._dispatcher = self._options.dispatcher - self._options.dispatcher.transfer_data(self._request) - except dispatch.DispatchException, e: - logging.warning('mod_pywebsocket: %s' % e) + except handshake.HandshakeException, e: + # Handshake for ws(s) failed. + self._logger.info('%s', e) + self.send_error(e.status) return False - except handshake.AbortedByUserException, e: - logging.info('mod_pywebsocket: %s' % e) - except Exception, e: - # Catch exception in transfer_data. - # In this case, handshake has been successful, so just log - # the exception and return False. - logging.info('mod_pywebsocket: %s' % e) - logging.info( - 'mod_pywebsocket: %s' % util.get_stack_trace()) - except dispatch.DispatchException, e: - logging.warning('mod_pywebsocket: %s' % e) - self.send_error(e.status) - except handshake.HandshakeException, e: - # Handshake for ws(s) failed. Assume http(s). - logging.info('mod_pywebsocket: %s' % e) - self.send_error(e.status) - except Exception, e: - logging.warning('mod_pywebsocket: %s' % e) - logging.warning('mod_pywebsocket: %s' % util.get_stack_trace()) + + request._dispatcher = self._options.dispatcher + self._options.dispatcher.transfer_data(request) + except handshake.AbortedByUserException, e: + self._logger.info('%s', e) return False def log_request(self, code='-', size='-'): """Override BaseHTTPServer.log_request.""" - logging.info('"%s" %s %s', - self.requestline, str(code), str(size)) + 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. - logging.warning('%s - %s' % - (self.address_string(), (args[0] % args[1:]))) + 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. @@ -544,8 +564,9 @@ def _alias_handlers(dispatcher, websock_handlers_map_file): fp.close() -def _main(): +def _build_option_parser(): parser = optparse.OptionParser() + parser.add_option('-H', '--server-host', '--server_host', dest='server_host', default='', @@ -576,6 +597,13 @@ def _main(): default=None, help=('WebSocket handlers scan directory. ' 'Must be a directory under websock_handlers.')) + parser.add_option('--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_option('-d', '--document-root', '--document_root', dest='document_root', default='.', help='Document root directory.') @@ -599,6 +627,15 @@ def _main(): choices=['debug', 'info', 'warning', 'warn', 'error', 'critical'], help='Log level.') + parser.add_option('--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_option('--log-max', '--log_max', dest='log_max', type='int', default=_DEFAULT_LOG_MAX_BYTES, help='Log maximum bytes') @@ -613,7 +650,39 @@ def _main(): parser.add_option('-q', '--queue', dest='request_queue_size', type='int', default=_DEFAULT_REQUEST_QUEUE_SIZE, help='request queue size') - options = parser.parse_args()[0] + + return parser + + +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) + + +def _main(args=None): + parser = _build_option_parser() + + options, args = parser.parse_args(args=args) + if args: + logging.critical('Unrecognized positional arguments: %r', args) + sys.exit(1) os.chdir(options.document_root) @@ -653,14 +722,24 @@ def _main(): options.scan_dir = options.websock_handlers try: + if options.thread_monitor_interval_in_sec > 0: + # Run a thread monitor to show the status of server threads for + # debugging. + ThreadMonitor(options.thread_monitor_interval_in_sec).start() + # 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.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) - _print_warnings_if_any(options.dispatcher) + warnings = options.dispatcher.source_warnings() + if warnings: + for warning in warnings: + logging.warning('mod_pywebsocket: %s' % warning) server = WebSocketServer(options) server.serve_forever()