#!/usr/bin/env python # # Copyright 2011, Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Standalone WebSocket server. Use this server to run mod_pywebsocket without Apache HTTP Server. Usage: python standalone.py [-p ] [-w ] [-s ] [-d ] [-m ] ... for other options, see _main below ... is the port number to use for ws:// connection. is the path to the root directory of HTML files. is the path to the root directory of WebSocket handlers. See __init__.py for details of and how to write WebSocket handlers. If this path is relative, is used as the base. is a path under the root directory. If specified, only the handlers under scan_dir are scanned. This is useful in saving scan time. Note: This server is derived from SocketServer.ThreadingMixIn. Hence a thread is used for each request. SECURITY WARNING: This uses CGIHTTPServer and CGIHTTPServer is not secure. It may execute arbitrary Python code or external programs. It should not be used outside a firewall. """ import BaseHTTPServer import CGIHTTPServer import SimpleHTTPServer import SocketServer import logging import logging.handlers import optparse import os import re import socket import sys _HAS_OPEN_SSL = False try: import OpenSSL.SSL _HAS_OPEN_SSL = True except ImportError: pass from mod_pywebsocket import common from mod_pywebsocket import dispatch from mod_pywebsocket import handshake from mod_pywebsocket import memorizingfile from mod_pywebsocket import util _DEFAULT_LOG_MAX_BYTES = 1024 * 256 _DEFAULT_LOG_BACKUP_COUNT = 5 _DEFAULT_REQUEST_QUEUE_SIZE = 128 # 1024 is practically large enough to contain WebSocket handshake lines. _MAX_MEMORIZED_LINES = 1024 def _print_warnings_if_any(dispatcher): warnings = dispatcher.source_warnings() if warnings: for warning in warnings: logging.warning('mod_pywebsocket: %s' % warning) class _StandaloneConnection(object): """Mimic mod_python mp_conn.""" def __init__(self, request_handler): """Construct an instance. Args: request_handler: A WebSocketRequestHandler instance. """ self._request_handler = request_handler def get_local_addr(self): """Getter to mimic mp_conn.local_addr.""" return (self._request_handler.server.server_name, self._request_handler.server.server_port) local_addr = property(get_local_addr) def get_remote_addr(self): """Getter to mimic mp_conn.remote_addr. Setting the property in __init__ won't work because the request handler is not initialized yet there.""" return self._request_handler.client_address remote_addr = property(get_remote_addr) def write(self, data): """Mimic mp_conn.write().""" return self._request_handler.wfile.write(data) def read(self, length): """Mimic mp_conn.read().""" return self._request_handler.rfile.read(length) def get_memorized_lines(self): """Get memorized lines.""" return self._request_handler.rfile.get_memorized_lines() def setblocking(self, blocking): self._request_handler.rfile._file._sock.setblocking(0) class _StandaloneRequest(object): """Mimic mod_python request.""" def __init__(self, request_handler, use_tls): """Construct an instance. Args: request_handler: A WebSocketRequestHandler instance. """ self._request_handler = request_handler self.connection = _StandaloneConnection(request_handler) self._use_tls = use_tls def get_uri(self): """Getter to mimic request.uri.""" return self._request_handler.path uri = property(get_uri) def get_method(self): """Getter to mimic request.method.""" return self._request_handler.command method = property(get_method) def get_headers_in(self): """Getter to mimic request.headers_in.""" return self._request_handler.headers headers_in = property(get_headers_in) def is_https(self): """Mimic request.is_https().""" return self._use_tls class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): """HTTPServer specialized for WebSocket.""" daemon_threads = True allow_reuse_address = True def __init__(self, server_address, RequestHandlerClass): """Override SocketServer.TCPServer.__init__ to set SSL enabled socket object to self.socket before server_bind and server_activate, if necessary. """ SocketServer.BaseServer.__init__( self, server_address, RequestHandlerClass) self.socket = self._create_socket() self.server_bind() self.server_activate() def _create_socket(self): socket_ = socket.socket(self.address_family, self.socket_type) if WebSocketServer.options.use_tls: ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) ctx.use_privatekey_file(WebSocketServer.options.private_key) ctx.use_certificate_file(WebSocketServer.options.certificate) socket_ = OpenSSL.SSL.Connection(ctx, socket_) return socket_ def handle_error(self, rquest, client_address): """Override SocketServer.handle_error.""" logging.error( ('Exception in processing request from: %r' % (client_address,)) + '\n' + util.get_stack_trace()) # Note: client_address is a tuple. To match it against %r, we need the # trailing comma. class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): """CGIHTTPRequestHandler specialized for WebSocket.""" def setup(self): """Override SocketServer.StreamRequestHandler.setup to wrap rfile with MemorizingFile. """ # Call superclass's setup to prepare rfile, wfile, etc. See setup # definition on the root class SocketServer.StreamRequestHandler to # understand what this does. CGIHTTPServer.CGIHTTPRequestHandler.setup(self) self.rfile = memorizingfile.MemorizingFile( self.rfile, max_memorized_lines=_MAX_MEMORIZED_LINES) def __init__(self, *args, **keywords): self._request = _StandaloneRequest( self, WebSocketRequestHandler.options.use_tls) self._dispatcher = WebSocketRequestHandler.options.dispatcher self._print_warnings_if_any() self._handshaker = handshake.Handshaker( self._request, self._dispatcher, allowDraft75=WebSocketRequestHandler.options.allow_draft75, strict=WebSocketRequestHandler.options.strict) CGIHTTPServer.CGIHTTPRequestHandler.__init__( self, *args, **keywords) def _print_warnings_if_any(self): warnings = self._dispatcher.source_warnings() if warnings: for warning in warnings: logging.warning('mod_pywebsocket: %s' % warning) def parse_request(self): """Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request. Return True to continue processing for HTTP(S), False otherwise. """ result = CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self) if result: try: self._handshaker.do_handshake() try: self._dispatcher.transfer_data(self._request) except Exception, e: # Catch exception in transfer_data. # In this case, handshake has been successful, so just log # the exception and return False. logging.info('mod_pywebsocket: %s' % e) logging.info( 'mod_pywebsocket: %s' % util.get_stack_trace()) return False except handshake.HandshakeError, e: # Handshake for ws(s) failed. Assume http(s). logging.info('mod_pywebsocket: %s' % e) return True except dispatch.DispatchError, e: logging.warning('mod_pywebsocket: %s' % e) return False except Exception, e: logging.warning('mod_pywebsocket: %s' % e) logging.warning('mod_pywebsocket: %s' % util.get_stack_trace()) return False return result def log_request(self, code='-', size='-'): """Override BaseHTTPServer.log_request.""" logging.info('"%s" %s %s', self.requestline, str(code), str(size)) def log_error(self, *args): """Override BaseHTTPServer.log_error.""" # Despite the name, this method is for warnings than for errors. # For example, HTTP status code is logged by this method. logging.warn('%s - %s' % (self.address_string(), (args[0] % args[1:]))) def is_cgi(self): """Test whether self.path corresponds to a CGI script. Add extra check that self.path doesn't contains .. Also check if the file is a executable file or not. If the file is not executable, it is handled as static file or dir rather than a CGI script. """ if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self): if '..' in self.path: return False # strip query parameter from request path resource_name = self.path.split('?', 2)[0] # convert resource_name into real path name in filesystem. scriptfile = self.translate_path(resource_name) if not os.path.isfile(scriptfile): return False if not self.is_executable(scriptfile): return False return True return False def _configure_logging(options): logger = logging.getLogger() logger.setLevel(logging.getLevelName(options.log_level.upper())) if options.log_file: handler = logging.handlers.RotatingFileHandler( options.log_file, 'a', options.log_max, options.log_count) else: handler = logging.StreamHandler() formatter = logging.Formatter( '[%(asctime)s] [%(levelname)s] %(name)s: %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) def _alias_handlers(dispatcher, websock_handlers_map_file): """Set aliases specified in websock_handler_map_file in dispatcher. Args: dispatcher: dispatch.Dispatcher instance websock_handler_map_file: alias map file """ fp = open(websock_handlers_map_file) try: for line in fp: if line[0] == '#' or line.isspace(): continue m = re.match('(\S+)\s+(\S+)', line) if not m: logging.warning('Wrong format in map file:' + line) continue try: dispatcher.add_resource_path_alias( m.group(1), m.group(2)) except dispatch.DispatchError, e: logging.error(str(e)) finally: fp.close() def _main(): parser = optparse.OptionParser() parser.add_option('-H', '--server-host', '--server_host', dest='server_host', default='', help='server hostname to listen to') parser.add_option('-p', '--port', dest='port', type='int', default=common.DEFAULT_WEB_SOCKET_PORT, help='port to listen to') parser.add_option('-w', '--websock-handlers', '--websock_handlers', dest='websock_handlers', default='.', help='WebSocket handlers root directory.') parser.add_option('-m', '--websock-handlers-map-file', '--websock_handlers_map_file', dest='websock_handlers_map_file', default=None, help=('WebSocket handlers map file. ' 'Each line consists of alias_resource_path and ' 'existing_resource_path, separated by spaces.')) parser.add_option('-s', '--scan-dir', '--scan_dir', dest='scan_dir', default=None, help=('WebSocket handlers scan directory. ' 'Must be a directory under websock_handlers.')) parser.add_option('-d', '--document-root', '--document_root', dest='document_root', default='.', help='Document root directory.') parser.add_option('-x', '--cgi-paths', '--cgi_paths', dest='cgi_paths', default=None, help=('CGI paths relative to document_root.' 'Comma-separated. (e.g -x /cgi,/htbin) ' 'Files under document_root/cgi_path are handled ' 'as CGI programs. Must be executable.')) parser.add_option('-t', '--tls', dest='use_tls', action='store_true', default=False, help='use TLS (wss://)') parser.add_option('-k', '--private-key', '--private_key', dest='private_key', default='', help='TLS private key file.') parser.add_option('-c', '--certificate', dest='certificate', default='', help='TLS certificate file.') parser.add_option('-l', '--log-file', '--log_file', dest='log_file', default='', help='Log file.') parser.add_option('--log-level', '--log_level', type='choice', dest='log_level', default='warn', choices=['debug', 'info', 'warning', 'warn', 'error', 'critical'], help='Log level.') parser.add_option('--log-max', '--log_max', dest='log_max', type='int', default=_DEFAULT_LOG_MAX_BYTES, help='Log maximum bytes') parser.add_option('--log-count', '--log_count', dest='log_count', type='int', default=_DEFAULT_LOG_BACKUP_COUNT, help='Log backup count') parser.add_option('--allow-draft75', dest='allow_draft75', action='store_true', default=False, help='Allow draft 75 handshake') parser.add_option('--strict', dest='strict', action='store_true', default=False, help='Strictly check handshake request') parser.add_option('-q', '--queue', dest='request_queue_size', type='int', default=_DEFAULT_REQUEST_QUEUE_SIZE, help='request queue size') options = parser.parse_args()[0] os.chdir(options.document_root) _configure_logging(options) SocketServer.TCPServer.request_queue_size = options.request_queue_size CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = [] if options.cgi_paths: CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = \ options.cgi_paths.split(',') if sys.platform in ('cygwin', 'win32'): cygwin_path = None # For Win32 Python, it is expected that CYGWIN_PATH # is set to a directory of cygwin binaries. # For example, websocket_server.py in Chromium sets CYGWIN_PATH to # full path of third_party/cygwin/bin. if 'CYGWIN_PATH' in os.environ: cygwin_path = os.environ['CYGWIN_PATH'] util.wrap_popen3_for_win(cygwin_path) def __check_script(scriptpath): return util.get_script_interp(scriptpath, cygwin_path) CGIHTTPServer.executable = __check_script if options.use_tls: if not _HAS_OPEN_SSL: logging.critical('To use TLS, install pyOpenSSL.') sys.exit(1) if not options.private_key or not options.certificate: logging.critical( 'To use TLS, specify private_key and certificate.') sys.exit(1) if not options.scan_dir: options.scan_dir = options.websock_handlers try: # Share a Dispatcher among request handlers to save time for # instantiation. Dispatcher can be shared because it is thread-safe. options.dispatcher = dispatch.Dispatcher(options.websock_handlers, options.scan_dir) if options.websock_handlers_map_file: _alias_handlers(options.dispatcher, options.websock_handlers_map_file) _print_warnings_if_any(options.dispatcher) WebSocketRequestHandler.options = options WebSocketServer.options = options server = WebSocketServer((options.server_host, options.port), WebSocketRequestHandler) server.serve_forever() except Exception, e: logging.critical('mod_pywebsocket: %s' % e) logging.critical('mod_pywebsocket: %s' % util.get_stack_trace()) sys.exit(1) if __name__ == '__main__': _main() # vi:sts=4 sw=4 et