michael@0: #!/usr/bin/env python michael@0: # michael@0: # Copyright 2011, Google Inc. michael@0: # All rights reserved. michael@0: # michael@0: # Redistribution and use in source and binary forms, with or without michael@0: # modification, are permitted provided that the following conditions are michael@0: # met: michael@0: # michael@0: # * Redistributions of source code must retain the above copyright michael@0: # notice, this list of conditions and the following disclaimer. michael@0: # * Redistributions in binary form must reproduce the above michael@0: # copyright notice, this list of conditions and the following disclaimer michael@0: # in the documentation and/or other materials provided with the michael@0: # distribution. michael@0: # * Neither the name of Google Inc. nor the names of its michael@0: # contributors may be used to endorse or promote products derived from michael@0: # this software without specific prior written permission. michael@0: # michael@0: # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS michael@0: # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT michael@0: # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR michael@0: # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT michael@0: # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, michael@0: # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT michael@0: # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, michael@0: # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY michael@0: # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT michael@0: # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE michael@0: # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. michael@0: michael@0: michael@0: """Standalone WebSocket server. michael@0: michael@0: BASIC USAGE michael@0: michael@0: Use this server to run mod_pywebsocket without Apache HTTP Server. michael@0: michael@0: Usage: michael@0: python standalone.py [-p ] [-w ] michael@0: [-s ] michael@0: [-d ] michael@0: [-m ] michael@0: ... for other options, see _main below ... michael@0: michael@0: is the port number to use for ws:// connection. michael@0: michael@0: is the path to the root directory of HTML files. michael@0: michael@0: is the path to the root directory of WebSocket handlers. michael@0: See __init__.py for details of and how to write WebSocket michael@0: handlers. If this path is relative, is used as the base. michael@0: michael@0: is a path under the root directory. If specified, only the michael@0: handlers under scan_dir are scanned. This is useful in saving scan time. michael@0: michael@0: michael@0: CONFIGURATION FILE michael@0: michael@0: You can also write a configuration file and use it by specifying the path to michael@0: the configuration file by --config option. Please write a configuration file michael@0: following the documentation of the Python ConfigParser library. Name of each michael@0: entry must be the long version argument name. E.g. to set log level to debug, michael@0: add the following line: michael@0: michael@0: log_level=debug michael@0: michael@0: For options which doesn't take value, please add some fake value. E.g. for michael@0: --tls option, add the following line: michael@0: michael@0: tls=True michael@0: michael@0: Note that tls will be enabled even if you write tls=False as the value part is michael@0: fake. michael@0: michael@0: When both a command line argument and a configuration file entry are set for michael@0: the same configuration item, the command line value will override one in the michael@0: configuration file. michael@0: michael@0: michael@0: THREADING michael@0: michael@0: This server is derived from SocketServer.ThreadingMixIn. Hence a thread is michael@0: used for each request. michael@0: michael@0: michael@0: SECURITY WARNING michael@0: michael@0: This uses CGIHTTPServer and CGIHTTPServer is not secure. michael@0: It may execute arbitrary Python code or external programs. It should not be michael@0: used outside a firewall. michael@0: """ michael@0: michael@0: import BaseHTTPServer michael@0: import CGIHTTPServer michael@0: import SimpleHTTPServer michael@0: import SocketServer michael@0: import ConfigParser michael@0: import httplib michael@0: import logging michael@0: import logging.handlers michael@0: import optparse michael@0: import os michael@0: import re michael@0: import select michael@0: import socket michael@0: import sys michael@0: import threading michael@0: import time michael@0: michael@0: _HAS_SSL = False michael@0: _HAS_OPEN_SSL = False michael@0: try: michael@0: import ssl michael@0: _HAS_SSL = True michael@0: except ImportError: michael@0: try: michael@0: import OpenSSL.SSL michael@0: _HAS_OPEN_SSL = True michael@0: except ImportError: michael@0: pass michael@0: michael@0: from mod_pywebsocket import common michael@0: from mod_pywebsocket import dispatch michael@0: from mod_pywebsocket import handshake michael@0: from mod_pywebsocket import http_header_util michael@0: from mod_pywebsocket import memorizingfile michael@0: from mod_pywebsocket import util michael@0: michael@0: michael@0: _DEFAULT_LOG_MAX_BYTES = 1024 * 256 michael@0: _DEFAULT_LOG_BACKUP_COUNT = 5 michael@0: michael@0: _DEFAULT_REQUEST_QUEUE_SIZE = 128 michael@0: michael@0: # 1024 is practically large enough to contain WebSocket handshake lines. michael@0: _MAX_MEMORIZED_LINES = 1024 michael@0: michael@0: michael@0: class _StandaloneConnection(object): michael@0: """Mimic mod_python mp_conn.""" michael@0: michael@0: def __init__(self, request_handler): michael@0: """Construct an instance. michael@0: michael@0: Args: michael@0: request_handler: A WebSocketRequestHandler instance. michael@0: """ michael@0: michael@0: self._request_handler = request_handler michael@0: michael@0: def get_local_addr(self): michael@0: """Getter to mimic mp_conn.local_addr.""" michael@0: michael@0: return (self._request_handler.server.server_name, michael@0: self._request_handler.server.server_port) michael@0: local_addr = property(get_local_addr) michael@0: michael@0: def get_remote_addr(self): michael@0: """Getter to mimic mp_conn.remote_addr. michael@0: michael@0: Setting the property in __init__ won't work because the request michael@0: handler is not initialized yet there.""" michael@0: michael@0: return self._request_handler.client_address michael@0: remote_addr = property(get_remote_addr) michael@0: michael@0: def write(self, data): michael@0: """Mimic mp_conn.write().""" michael@0: michael@0: return self._request_handler.wfile.write(data) michael@0: michael@0: def read(self, length): michael@0: """Mimic mp_conn.read().""" michael@0: michael@0: return self._request_handler.rfile.read(length) michael@0: michael@0: def get_memorized_lines(self): michael@0: """Get memorized lines.""" michael@0: michael@0: return self._request_handler.rfile.get_memorized_lines() michael@0: michael@0: michael@0: class _StandaloneRequest(object): michael@0: """Mimic mod_python request.""" michael@0: michael@0: def __init__(self, request_handler, use_tls): michael@0: """Construct an instance. michael@0: michael@0: Args: michael@0: request_handler: A WebSocketRequestHandler instance. michael@0: """ michael@0: michael@0: self._logger = util.get_class_logger(self) michael@0: michael@0: self._request_handler = request_handler michael@0: self.connection = _StandaloneConnection(request_handler) michael@0: self._use_tls = use_tls michael@0: self.headers_in = request_handler.headers michael@0: michael@0: def get_uri(self): michael@0: """Getter to mimic request.uri.""" michael@0: michael@0: return self._request_handler.path michael@0: uri = property(get_uri) michael@0: michael@0: def get_method(self): michael@0: """Getter to mimic request.method.""" michael@0: michael@0: return self._request_handler.command michael@0: method = property(get_method) michael@0: michael@0: def is_https(self): michael@0: """Mimic request.is_https().""" michael@0: michael@0: return self._use_tls michael@0: michael@0: def _drain_received_data(self): michael@0: """Don't use this method from WebSocket handler. Drains unread data michael@0: in the receive buffer. michael@0: """ michael@0: michael@0: raw_socket = self._request_handler.connection michael@0: drained_data = util.drain_received_data(raw_socket) michael@0: michael@0: if drained_data: michael@0: self._logger.debug( michael@0: 'Drained data following close frame: %r', drained_data) michael@0: michael@0: michael@0: class _StandaloneSSLConnection(object): michael@0: """A wrapper class for OpenSSL.SSL.Connection to provide makefile method michael@0: which is not supported by the class. michael@0: """ michael@0: michael@0: def __init__(self, connection): michael@0: self._connection = connection michael@0: michael@0: def __getattribute__(self, name): michael@0: if name in ('_connection', 'makefile'): michael@0: return object.__getattribute__(self, name) michael@0: return self._connection.__getattribute__(name) michael@0: michael@0: def __setattr__(self, name, value): michael@0: if name in ('_connection', 'makefile'): michael@0: return object.__setattr__(self, name, value) michael@0: return self._connection.__setattr__(name, value) michael@0: michael@0: def makefile(self, mode='r', bufsize=-1): michael@0: return socket._fileobject(self._connection, mode, bufsize) michael@0: michael@0: michael@0: class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): michael@0: """HTTPServer specialized for WebSocket.""" michael@0: michael@0: # Overrides SocketServer.ThreadingMixIn.daemon_threads michael@0: daemon_threads = True michael@0: # Overrides BaseHTTPServer.HTTPServer.allow_reuse_address michael@0: allow_reuse_address = True michael@0: michael@0: def __init__(self, options): michael@0: """Override SocketServer.TCPServer.__init__ to set SSL enabled michael@0: socket object to self.socket before server_bind and server_activate, michael@0: if necessary. michael@0: """ michael@0: michael@0: self._logger = util.get_class_logger(self) michael@0: michael@0: self.request_queue_size = options.request_queue_size michael@0: self.__ws_is_shut_down = threading.Event() michael@0: self.__ws_serving = False michael@0: michael@0: SocketServer.BaseServer.__init__( michael@0: self, (options.server_host, options.port), WebSocketRequestHandler) michael@0: michael@0: # Expose the options object to allow handler objects access it. We name michael@0: # it with websocket_ prefix to avoid conflict. michael@0: self.websocket_server_options = options michael@0: michael@0: self._create_sockets() michael@0: self.server_bind() michael@0: self.server_activate() michael@0: michael@0: def _create_sockets(self): michael@0: self.server_name, self.server_port = self.server_address michael@0: self._sockets = [] michael@0: if not self.server_name: michael@0: # On platforms that doesn't support IPv6, the first bind fails. michael@0: # On platforms that supports IPv6 michael@0: # - If it binds both IPv4 and IPv6 on call with AF_INET6, the michael@0: # first bind succeeds and the second fails (we'll see 'Address michael@0: # already in use' error). michael@0: # - If it binds only IPv6 on call with AF_INET6, both call are michael@0: # expected to succeed to listen both protocol. michael@0: addrinfo_array = [ michael@0: (socket.AF_INET6, socket.SOCK_STREAM, '', '', ''), michael@0: (socket.AF_INET, socket.SOCK_STREAM, '', '', '')] michael@0: else: michael@0: addrinfo_array = socket.getaddrinfo(self.server_name, michael@0: self.server_port, michael@0: socket.AF_UNSPEC, michael@0: socket.SOCK_STREAM, michael@0: socket.IPPROTO_TCP) michael@0: for addrinfo in addrinfo_array: michael@0: self._logger.info('Create socket on: %r', addrinfo) michael@0: family, socktype, proto, canonname, sockaddr = addrinfo michael@0: try: michael@0: socket_ = socket.socket(family, socktype) michael@0: except Exception, e: michael@0: self._logger.info('Skip by failure: %r', e) michael@0: continue michael@0: if self.websocket_server_options.use_tls: michael@0: if _HAS_SSL: michael@0: socket_ = ssl.wrap_socket(socket_, michael@0: keyfile=self.websocket_server_options.private_key, michael@0: certfile=self.websocket_server_options.certificate, michael@0: ssl_version=ssl.PROTOCOL_SSLv23) michael@0: if _HAS_OPEN_SSL: michael@0: ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) michael@0: ctx.use_privatekey_file( michael@0: self.websocket_server_options.private_key) michael@0: ctx.use_certificate_file( michael@0: self.websocket_server_options.certificate) michael@0: socket_ = OpenSSL.SSL.Connection(ctx, socket_) michael@0: self._sockets.append((socket_, addrinfo)) michael@0: michael@0: def server_bind(self): michael@0: """Override SocketServer.TCPServer.server_bind to enable multiple michael@0: sockets bind. michael@0: """ michael@0: michael@0: failed_sockets = [] michael@0: michael@0: for socketinfo in self._sockets: michael@0: socket_, addrinfo = socketinfo michael@0: self._logger.info('Bind on: %r', addrinfo) michael@0: if self.allow_reuse_address: michael@0: socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) michael@0: try: michael@0: socket_.bind(self.server_address) michael@0: except Exception, e: michael@0: self._logger.info('Skip by failure: %r', e) michael@0: socket_.close() michael@0: failed_sockets.append(socketinfo) michael@0: michael@0: for socketinfo in failed_sockets: michael@0: self._sockets.remove(socketinfo) michael@0: michael@0: def server_activate(self): michael@0: """Override SocketServer.TCPServer.server_activate to enable multiple michael@0: sockets listen. michael@0: """ michael@0: michael@0: failed_sockets = [] michael@0: michael@0: for socketinfo in self._sockets: michael@0: socket_, addrinfo = socketinfo michael@0: self._logger.info('Listen on: %r', addrinfo) michael@0: try: michael@0: socket_.listen(self.request_queue_size) michael@0: except Exception, e: michael@0: self._logger.info('Skip by failure: %r', e) michael@0: socket_.close() michael@0: failed_sockets.append(socketinfo) michael@0: michael@0: for socketinfo in failed_sockets: michael@0: self._sockets.remove(socketinfo) michael@0: michael@0: def server_close(self): michael@0: """Override SocketServer.TCPServer.server_close to enable multiple michael@0: sockets close. michael@0: """ michael@0: michael@0: for socketinfo in self._sockets: michael@0: socket_, addrinfo = socketinfo michael@0: self._logger.info('Close on: %r', addrinfo) michael@0: socket_.close() michael@0: michael@0: def fileno(self): michael@0: """Override SocketServer.TCPServer.fileno.""" michael@0: michael@0: self._logger.critical('Not supported: fileno') michael@0: return self._sockets[0][0].fileno() michael@0: michael@0: def handle_error(self, rquest, client_address): michael@0: """Override SocketServer.handle_error.""" michael@0: michael@0: self._logger.error( michael@0: 'Exception in processing request from: %r\n%s', michael@0: client_address, michael@0: util.get_stack_trace()) michael@0: # Note: client_address is a tuple. michael@0: michael@0: def get_request(self): michael@0: """Override TCPServer.get_request to wrap OpenSSL.SSL.Connection michael@0: object with _StandaloneSSLConnection to provide makefile method. We michael@0: cannot substitute OpenSSL.SSL.Connection.makefile since it's readonly michael@0: attribute. michael@0: """ michael@0: michael@0: accepted_socket, client_address = self.socket.accept() michael@0: if self.websocket_server_options.use_tls and _HAS_OPEN_SSL: michael@0: accepted_socket = _StandaloneSSLConnection(accepted_socket) michael@0: return accepted_socket, client_address michael@0: michael@0: def serve_forever(self, poll_interval=0.5): michael@0: """Override SocketServer.BaseServer.serve_forever.""" michael@0: michael@0: self.__ws_serving = True michael@0: self.__ws_is_shut_down.clear() michael@0: handle_request = self.handle_request michael@0: if hasattr(self, '_handle_request_noblock'): michael@0: handle_request = self._handle_request_noblock michael@0: else: michael@0: self._logger.warning('Fallback to blocking request handler') michael@0: try: michael@0: while self.__ws_serving: michael@0: r, w, e = select.select( michael@0: [socket_[0] for socket_ in self._sockets], michael@0: [], [], poll_interval) michael@0: for socket_ in r: michael@0: self.socket = socket_ michael@0: handle_request() michael@0: self.socket = None michael@0: finally: michael@0: self.__ws_is_shut_down.set() michael@0: michael@0: def shutdown(self): michael@0: """Override SocketServer.BaseServer.shutdown.""" michael@0: michael@0: self.__ws_serving = False michael@0: self.__ws_is_shut_down.wait() michael@0: michael@0: michael@0: class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): michael@0: """CGIHTTPRequestHandler specialized for WebSocket.""" michael@0: michael@0: # Use httplib.HTTPMessage instead of mimetools.Message. michael@0: MessageClass = httplib.HTTPMessage michael@0: michael@0: def setup(self): michael@0: """Override SocketServer.StreamRequestHandler.setup to wrap rfile michael@0: with MemorizingFile. michael@0: michael@0: This method will be called by BaseRequestHandler's constructor michael@0: before calling BaseHTTPRequestHandler.handle. michael@0: BaseHTTPRequestHandler.handle will call michael@0: BaseHTTPRequestHandler.handle_one_request and it will call michael@0: WebSocketRequestHandler.parse_request. michael@0: """ michael@0: michael@0: # Call superclass's setup to prepare rfile, wfile, etc. See setup michael@0: # definition on the root class SocketServer.StreamRequestHandler to michael@0: # understand what this does. michael@0: CGIHTTPServer.CGIHTTPRequestHandler.setup(self) michael@0: michael@0: self.rfile = memorizingfile.MemorizingFile( michael@0: self.rfile, michael@0: max_memorized_lines=_MAX_MEMORIZED_LINES) michael@0: michael@0: def __init__(self, request, client_address, server): michael@0: self._logger = util.get_class_logger(self) michael@0: michael@0: self._options = server.websocket_server_options michael@0: michael@0: # Overrides CGIHTTPServerRequestHandler.cgi_directories. michael@0: self.cgi_directories = self._options.cgi_directories michael@0: # Replace CGIHTTPRequestHandler.is_executable method. michael@0: if self._options.is_executable_method is not None: michael@0: self.is_executable = self._options.is_executable_method michael@0: michael@0: # This actually calls BaseRequestHandler.__init__. michael@0: CGIHTTPServer.CGIHTTPRequestHandler.__init__( michael@0: self, request, client_address, server) michael@0: michael@0: def parse_request(self): michael@0: """Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request. michael@0: michael@0: Return True to continue processing for HTTP(S), False otherwise. michael@0: michael@0: See BaseHTTPRequestHandler.handle_one_request method which calls michael@0: this method to understand how the return value will be handled. michael@0: """ michael@0: michael@0: # We hook parse_request method, but also call the original michael@0: # CGIHTTPRequestHandler.parse_request since when we return False, michael@0: # CGIHTTPRequestHandler.handle_one_request continues processing and michael@0: # it needs variables set by CGIHTTPRequestHandler.parse_request. michael@0: # michael@0: # Variables set by this method will be also used by WebSocket request michael@0: # handling (self.path, self.command, self.requestline, etc. See also michael@0: # how _StandaloneRequest's members are implemented using these michael@0: # attributes). michael@0: if not CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self): michael@0: return False michael@0: host, port, resource = http_header_util.parse_uri(self.path) michael@0: if resource is None: michael@0: self._logger.info('Invalid URI: %r', self.path) michael@0: self._logger.info('Fallback to CGIHTTPRequestHandler') michael@0: return True michael@0: server_options = self.server.websocket_server_options michael@0: if host is not None: michael@0: validation_host = server_options.validation_host michael@0: if validation_host is not None and host != validation_host: michael@0: self._logger.info('Invalid host: %r (expected: %r)', michael@0: host, michael@0: validation_host) michael@0: self._logger.info('Fallback to CGIHTTPRequestHandler') michael@0: return True michael@0: if port is not None: michael@0: validation_port = server_options.validation_port michael@0: if validation_port is not None and port != validation_port: michael@0: self._logger.info('Invalid port: %r (expected: %r)', michael@0: port, michael@0: validation_port) michael@0: self._logger.info('Fallback to CGIHTTPRequestHandler') michael@0: return True michael@0: self.path = resource michael@0: michael@0: request = _StandaloneRequest(self, self._options.use_tls) michael@0: michael@0: try: michael@0: # Fallback to default http handler for request paths for which michael@0: # we don't have request handlers. michael@0: if not self._options.dispatcher.get_handler_suite(self.path): michael@0: self._logger.info('No handler for resource: %r', michael@0: self.path) michael@0: self._logger.info('Fallback to CGIHTTPRequestHandler') michael@0: return True michael@0: except dispatch.DispatchException, e: michael@0: self._logger.info('%s', e) michael@0: self.send_error(e.status) michael@0: return False michael@0: michael@0: # If any Exceptions without except clause setup (including michael@0: # DispatchException) is raised below this point, it will be caught michael@0: # and logged by WebSocketServer. michael@0: michael@0: try: michael@0: try: michael@0: handshake.do_handshake( michael@0: request, michael@0: self._options.dispatcher, michael@0: allowDraft75=self._options.allow_draft75, michael@0: strict=self._options.strict) michael@0: except handshake.VersionException, e: michael@0: self._logger.info('%s', e) michael@0: self.send_response(common.HTTP_STATUS_BAD_REQUEST) michael@0: self.send_header(common.SEC_WEBSOCKET_VERSION_HEADER, michael@0: e.supported_versions) michael@0: self.end_headers() michael@0: return False michael@0: except handshake.HandshakeException, e: michael@0: # Handshake for ws(s) failed. michael@0: self._logger.info('%s', e) michael@0: self.send_error(e.status) michael@0: return False michael@0: michael@0: request._dispatcher = self._options.dispatcher michael@0: self._options.dispatcher.transfer_data(request) michael@0: except handshake.AbortedByUserException, e: michael@0: self._logger.info('%s', e) michael@0: return False michael@0: michael@0: def log_request(self, code='-', size='-'): michael@0: """Override BaseHTTPServer.log_request.""" michael@0: michael@0: self._logger.info('"%s" %s %s', michael@0: self.requestline, str(code), str(size)) michael@0: michael@0: def log_error(self, *args): michael@0: """Override BaseHTTPServer.log_error.""" michael@0: michael@0: # Despite the name, this method is for warnings than for errors. michael@0: # For example, HTTP status code is logged by this method. michael@0: self._logger.warning('%s - %s', michael@0: self.address_string(), michael@0: args[0] % args[1:]) michael@0: michael@0: def is_cgi(self): michael@0: """Test whether self.path corresponds to a CGI script. michael@0: michael@0: Add extra check that self.path doesn't contains .. michael@0: Also check if the file is a executable file or not. michael@0: If the file is not executable, it is handled as static file or dir michael@0: rather than a CGI script. michael@0: """ michael@0: michael@0: if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self): michael@0: if '..' in self.path: michael@0: return False michael@0: # strip query parameter from request path michael@0: resource_name = self.path.split('?', 2)[0] michael@0: # convert resource_name into real path name in filesystem. michael@0: scriptfile = self.translate_path(resource_name) michael@0: if not os.path.isfile(scriptfile): michael@0: return False michael@0: if not self.is_executable(scriptfile): michael@0: return False michael@0: return True michael@0: return False michael@0: michael@0: michael@0: def _configure_logging(options): michael@0: logger = logging.getLogger() michael@0: logger.setLevel(logging.getLevelName(options.log_level.upper())) michael@0: if options.log_file: michael@0: handler = logging.handlers.RotatingFileHandler( michael@0: options.log_file, 'a', options.log_max, options.log_count) michael@0: else: michael@0: handler = logging.StreamHandler() michael@0: formatter = logging.Formatter( michael@0: '[%(asctime)s] [%(levelname)s] %(name)s: %(message)s') michael@0: handler.setFormatter(formatter) michael@0: logger.addHandler(handler) michael@0: michael@0: michael@0: def _alias_handlers(dispatcher, websock_handlers_map_file): michael@0: """Set aliases specified in websock_handler_map_file in dispatcher. michael@0: michael@0: Args: michael@0: dispatcher: dispatch.Dispatcher instance michael@0: websock_handler_map_file: alias map file michael@0: """ michael@0: michael@0: fp = open(websock_handlers_map_file) michael@0: try: michael@0: for line in fp: michael@0: if line[0] == '#' or line.isspace(): michael@0: continue michael@0: m = re.match('(\S+)\s+(\S+)', line) michael@0: if not m: michael@0: logging.warning('Wrong format in map file:' + line) michael@0: continue michael@0: try: michael@0: dispatcher.add_resource_path_alias( michael@0: m.group(1), m.group(2)) michael@0: except dispatch.DispatchException, e: michael@0: logging.error(str(e)) michael@0: finally: michael@0: fp.close() michael@0: michael@0: michael@0: def _build_option_parser(): michael@0: parser = optparse.OptionParser() michael@0: michael@0: parser.add_option('--config', dest='config_file', type='string', michael@0: default=None, michael@0: help=('Path to configuration file. See the file comment ' michael@0: 'at the top of this file for the configuration ' michael@0: 'file format')) michael@0: parser.add_option('-H', '--server-host', '--server_host', michael@0: dest='server_host', michael@0: default='', michael@0: help='server hostname to listen to') michael@0: parser.add_option('-V', '--validation-host', '--validation_host', michael@0: dest='validation_host', michael@0: default=None, michael@0: help='server hostname to validate in absolute path.') michael@0: parser.add_option('-p', '--port', dest='port', type='int', michael@0: default=common.DEFAULT_WEB_SOCKET_PORT, michael@0: help='port to listen to') michael@0: parser.add_option('-P', '--validation-port', '--validation_port', michael@0: dest='validation_port', type='int', michael@0: default=None, michael@0: help='server port to validate in absolute path.') michael@0: parser.add_option('-w', '--websock-handlers', '--websock_handlers', michael@0: dest='websock_handlers', michael@0: default='.', michael@0: help='WebSocket handlers root directory.') michael@0: parser.add_option('-m', '--websock-handlers-map-file', michael@0: '--websock_handlers_map_file', michael@0: dest='websock_handlers_map_file', michael@0: default=None, michael@0: help=('WebSocket handlers map file. ' michael@0: 'Each line consists of alias_resource_path and ' michael@0: 'existing_resource_path, separated by spaces.')) michael@0: parser.add_option('-s', '--scan-dir', '--scan_dir', dest='scan_dir', michael@0: default=None, michael@0: help=('WebSocket handlers scan directory. ' michael@0: 'Must be a directory under websock_handlers.')) michael@0: parser.add_option('--allow-handlers-outside-root-dir', michael@0: '--allow_handlers_outside_root_dir', michael@0: dest='allow_handlers_outside_root_dir', michael@0: action='store_true', michael@0: default=False, michael@0: help=('Scans WebSocket handlers even if their canonical ' michael@0: 'path is not under websock_handlers.')) michael@0: parser.add_option('-d', '--document-root', '--document_root', michael@0: dest='document_root', default='.', michael@0: help='Document root directory.') michael@0: parser.add_option('-x', '--cgi-paths', '--cgi_paths', dest='cgi_paths', michael@0: default=None, michael@0: help=('CGI paths relative to document_root.' michael@0: 'Comma-separated. (e.g -x /cgi,/htbin) ' michael@0: 'Files under document_root/cgi_path are handled ' michael@0: 'as CGI programs. Must be executable.')) michael@0: parser.add_option('-t', '--tls', dest='use_tls', action='store_true', michael@0: default=False, help='use TLS (wss://)') michael@0: parser.add_option('-k', '--private-key', '--private_key', michael@0: dest='private_key', michael@0: default='', help='TLS private key file.') michael@0: parser.add_option('-c', '--certificate', dest='certificate', michael@0: default='', help='TLS certificate file.') michael@0: parser.add_option('-l', '--log-file', '--log_file', dest='log_file', michael@0: default='', help='Log file.') michael@0: parser.add_option('--log-level', '--log_level', type='choice', michael@0: dest='log_level', default='warn', michael@0: choices=['debug', 'info', 'warning', 'warn', 'error', michael@0: 'critical'], michael@0: help='Log level.') michael@0: parser.add_option('--thread-monitor-interval-in-sec', michael@0: '--thread_monitor_interval_in_sec', michael@0: dest='thread_monitor_interval_in_sec', michael@0: type='int', default=-1, michael@0: help=('If positive integer is specified, run a thread ' michael@0: 'monitor to show the status of server threads ' michael@0: 'periodically in the specified inteval in ' michael@0: 'second. If non-positive integer is specified, ' michael@0: 'disable the thread monitor.')) michael@0: parser.add_option('--log-max', '--log_max', dest='log_max', type='int', michael@0: default=_DEFAULT_LOG_MAX_BYTES, michael@0: help='Log maximum bytes') michael@0: parser.add_option('--log-count', '--log_count', dest='log_count', michael@0: type='int', default=_DEFAULT_LOG_BACKUP_COUNT, michael@0: help='Log backup count') michael@0: parser.add_option('--allow-draft75', dest='allow_draft75', michael@0: action='store_true', default=False, michael@0: help='Allow draft 75 handshake') michael@0: parser.add_option('--strict', dest='strict', action='store_true', michael@0: default=False, help='Strictly check handshake request') michael@0: parser.add_option('-q', '--queue', dest='request_queue_size', type='int', michael@0: default=_DEFAULT_REQUEST_QUEUE_SIZE, michael@0: help='request queue size') michael@0: michael@0: return parser michael@0: michael@0: michael@0: class ThreadMonitor(threading.Thread): michael@0: daemon = True michael@0: michael@0: def __init__(self, interval_in_sec): michael@0: threading.Thread.__init__(self, name='ThreadMonitor') michael@0: michael@0: self._logger = util.get_class_logger(self) michael@0: michael@0: self._interval_in_sec = interval_in_sec michael@0: michael@0: def run(self): michael@0: while True: michael@0: thread_name_list = [] michael@0: for thread in threading.enumerate(): michael@0: thread_name_list.append(thread.name) michael@0: self._logger.info( michael@0: "%d active threads: %s", michael@0: threading.active_count(), michael@0: ', '.join(thread_name_list)) michael@0: time.sleep(self._interval_in_sec) michael@0: michael@0: michael@0: def _parse_args_and_config(args): michael@0: parser = _build_option_parser() michael@0: michael@0: # First, parse options without configuration file. michael@0: temporary_options, temporary_args = parser.parse_args(args=args) michael@0: if temporary_args: michael@0: logging.critical( michael@0: 'Unrecognized positional arguments: %r', temporary_args) michael@0: sys.exit(1) michael@0: michael@0: if temporary_options.config_file: michael@0: try: michael@0: config_fp = open(temporary_options.config_file, 'r') michael@0: except IOError, e: michael@0: logging.critical( michael@0: 'Failed to open configuration file %r: %r', michael@0: temporary_options.config_file, michael@0: e) michael@0: sys.exit(1) michael@0: michael@0: config_parser = ConfigParser.SafeConfigParser() michael@0: config_parser.readfp(config_fp) michael@0: config_fp.close() michael@0: michael@0: args_from_config = [] michael@0: for name, value in config_parser.items('pywebsocket'): michael@0: args_from_config.append('--' + name) michael@0: args_from_config.append(value) michael@0: if args is None: michael@0: args = args_from_config michael@0: else: michael@0: args = args_from_config + args michael@0: return parser.parse_args(args=args) michael@0: else: michael@0: return temporary_options, temporary_args michael@0: michael@0: michael@0: def _main(args=None): michael@0: options, args = _parse_args_and_config(args=args) michael@0: michael@0: os.chdir(options.document_root) michael@0: michael@0: _configure_logging(options) michael@0: michael@0: # TODO(tyoshino): Clean up initialization of CGI related values. Move some michael@0: # of code here to WebSocketRequestHandler class if it's better. michael@0: options.cgi_directories = [] michael@0: options.is_executable_method = None michael@0: if options.cgi_paths: michael@0: options.cgi_directories = options.cgi_paths.split(',') michael@0: if sys.platform in ('cygwin', 'win32'): michael@0: cygwin_path = None michael@0: # For Win32 Python, it is expected that CYGWIN_PATH michael@0: # is set to a directory of cygwin binaries. michael@0: # For example, websocket_server.py in Chromium sets CYGWIN_PATH to michael@0: # full path of third_party/cygwin/bin. michael@0: if 'CYGWIN_PATH' in os.environ: michael@0: cygwin_path = os.environ['CYGWIN_PATH'] michael@0: util.wrap_popen3_for_win(cygwin_path) michael@0: michael@0: def __check_script(scriptpath): michael@0: return util.get_script_interp(scriptpath, cygwin_path) michael@0: michael@0: options.is_executable_method = __check_script michael@0: michael@0: if options.use_tls: michael@0: if not (_HAS_SSL or _HAS_OPEN_SSL): michael@0: logging.critical('TLS support requires ssl or pyOpenSSL.') michael@0: sys.exit(1) michael@0: if not options.private_key or not options.certificate: michael@0: logging.critical( michael@0: 'To use TLS, specify private_key and certificate.') michael@0: sys.exit(1) michael@0: michael@0: if not options.scan_dir: michael@0: options.scan_dir = options.websock_handlers michael@0: michael@0: try: michael@0: if options.thread_monitor_interval_in_sec > 0: michael@0: # Run a thread monitor to show the status of server threads for michael@0: # debugging. michael@0: ThreadMonitor(options.thread_monitor_interval_in_sec).start() michael@0: michael@0: # Share a Dispatcher among request handlers to save time for michael@0: # instantiation. Dispatcher can be shared because it is thread-safe. michael@0: options.dispatcher = dispatch.Dispatcher( michael@0: options.websock_handlers, michael@0: options.scan_dir, michael@0: options.allow_handlers_outside_root_dir) michael@0: if options.websock_handlers_map_file: michael@0: _alias_handlers(options.dispatcher, michael@0: options.websock_handlers_map_file) michael@0: warnings = options.dispatcher.source_warnings() michael@0: if warnings: michael@0: for warning in warnings: michael@0: logging.warning('mod_pywebsocket: %s' % warning) michael@0: michael@0: server = WebSocketServer(options) michael@0: server.serve_forever() michael@0: except Exception, e: michael@0: logging.critical('mod_pywebsocket: %s' % e) michael@0: logging.critical('mod_pywebsocket: %s' % util.get_stack_trace()) michael@0: sys.exit(1) michael@0: michael@0: michael@0: if __name__ == '__main__': michael@0: _main(sys.argv[1:]) michael@0: michael@0: michael@0: # vi:sts=4 sw=4 et