1 #!/usr/bin/env python 2 # 3 # Copyright 2009, Google Inc. 4 # All rights reserved. 5 # 6 # Redistribution and use in source and binary forms, with or without 7 # modification, are permitted provided that the following conditions are 8 # met: 9 # 10 # * Redistributions of source code must retain the above copyright 11 # notice, this list of conditions and the following disclaimer. 12 # * Redistributions in binary form must reproduce the above 13 # copyright notice, this list of conditions and the following disclaimer 14 # in the documentation and/or other materials provided with the 15 # distribution. 16 # * Neither the name of Google Inc. nor the names of its 17 # contributors may be used to endorse or promote products derived from 18 # this software without specific prior written permission. 19 # 20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 32 33 """Standalone Web Socket server. 34 35 Use this server to run mod_pywebsocket without Apache HTTP Server. 36 37 Usage: 38 python standalone.py [-p <ws_port>] [-w <websock_handlers>] 39 [-s <scan_dir>] 40 [-d <document_root>] 41 [-m <websock_handlers_map_file>] 42 ... for other options, see _main below ... 43 44 <ws_port> is the port number to use for ws:// connection. 45 46 <document_root> is the path to the root directory of HTML files. 47 48 <websock_handlers> is the path to the root directory of Web Socket handlers. 49 See __init__.py for details of <websock_handlers> and how to write Web Socket 50 handlers. If this path is relative, <document_root> is used as the base. 51 52 <scan_dir> is a path under the root directory. If specified, only the handlers 53 under scan_dir are scanned. This is useful in saving scan time. 54 55 Note: 56 This server is derived from SocketServer.ThreadingMixIn. Hence a thread is 57 used for each request. 58 """ 59 60 import BaseHTTPServer 61 import CGIHTTPServer 62 import SimpleHTTPServer 63 import SocketServer 64 import logging 65 import logging.handlers 66 import optparse 67 import os 68 import re 69 import socket 70 import sys 71 72 _HAS_OPEN_SSL = False 73 try: 74 import OpenSSL.SSL 75 _HAS_OPEN_SSL = True 76 except ImportError: 77 pass 78 79 import dispatch 80 import handshake 81 import memorizingfile 82 import util 83 84 85 _LOG_LEVELS = { 86 'debug': logging.DEBUG, 87 'info': logging.INFO, 88 'warn': logging.WARN, 89 'error': logging.ERROR, 90 'critical': logging.CRITICAL}; 91 92 _DEFAULT_LOG_MAX_BYTES = 1024 * 256 93 _DEFAULT_LOG_BACKUP_COUNT = 5 94 95 _DEFAULT_REQUEST_QUEUE_SIZE = 128 96 97 # 1024 is practically large enough to contain WebSocket handshake lines. 98 _MAX_MEMORIZED_LINES = 1024 99 100 def _print_warnings_if_any(dispatcher): 101 warnings = dispatcher.source_warnings() 102 if warnings: 103 for warning in warnings: 104 logging.warning('mod_pywebsocket: %s' % warning) 105 106 107 class _StandaloneConnection(object): 108 """Mimic mod_python mp_conn.""" 109 110 def __init__(self, request_handler): 111 """Construct an instance. 112 113 Args: 114 request_handler: A WebSocketRequestHandler instance. 115 """ 116 self._request_handler = request_handler 117 118 def get_local_addr(self): 119 """Getter to mimic mp_conn.local_addr.""" 120 return (self._request_handler.server.server_name, 121 self._request_handler.server.server_port) 122 local_addr = property(get_local_addr) 123 124 def get_remote_addr(self): 125 """Getter to mimic mp_conn.remote_addr. 126 127 Setting the property in __init__ won't work because the request 128 handler is not initialized yet there.""" 129 return self._request_handler.client_address 130 remote_addr = property(get_remote_addr) 131 132 def write(self, data): 133 """Mimic mp_conn.write().""" 134 return self._request_handler.wfile.write(data) 135 136 def read(self, length): 137 """Mimic mp_conn.read().""" 138 return self._request_handler.rfile.read(length) 139 140 def get_memorized_lines(self): 141 """Get memorized lines.""" 142 return self._request_handler.rfile.get_memorized_lines() 143 144 145 class _StandaloneRequest(object): 146 """Mimic mod_python request.""" 147 148 def __init__(self, request_handler, use_tls): 149 """Construct an instance. 150 151 Args: 152 request_handler: A WebSocketRequestHandler instance. 153 """ 154 self._request_handler = request_handler 155 self.connection = _StandaloneConnection(request_handler) 156 self._use_tls = use_tls 157 158 def get_uri(self): 159 """Getter to mimic request.uri.""" 160 return self._request_handler.path 161 uri = property(get_uri) 162 163 def get_headers_in(self): 164 """Getter to mimic request.headers_in.""" 165 return self._request_handler.headers 166 headers_in = property(get_headers_in) 167 168 def is_https(self): 169 """Mimic request.is_https().""" 170 return self._use_tls 171 172 173 class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): 174 """HTTPServer specialized for Web Socket.""" 175 176 SocketServer.ThreadingMixIn.daemon_threads = True 177 178 def __init__(self, server_address, RequestHandlerClass): 179 """Override SocketServer.BaseServer.__init__.""" 180 181 SocketServer.BaseServer.__init__( 182 self, server_address, RequestHandlerClass) 183 self.socket = self._create_socket() 184 self.server_bind() 185 self.server_activate() 186 187 def _create_socket(self): 188 socket_ = socket.socket(self.address_family, self.socket_type) 189 if WebSocketServer.options.use_tls: 190 ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) 191 ctx.use_privatekey_file(WebSocketServer.options.private_key) 192 ctx.use_certificate_file(WebSocketServer.options.certificate) 193 socket_ = OpenSSL.SSL.Connection(ctx, socket_) 194 return socket_ 195 196 def handle_error(self, rquest, client_address): 197 """Override SocketServer.handle_error.""" 198 199 logging.error( 200 ('Exception in processing request from: %r' % (client_address,)) + 201 '\n' + util.get_stack_trace()) 202 # Note: client_address is a tuple. To match it against %r, we need the 203 # trailing comma. 204 205 206 class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): 207 """CGIHTTPRequestHandler specialized for Web Socket.""" 208 209 def setup(self): 210 """Override SocketServer.StreamRequestHandler.setup.""" 211 212 self.connection = self.request 213 self.rfile = memorizingfile.MemorizingFile( 214 socket._fileobject(self.request, 'rb', self.rbufsize), 215 max_memorized_lines=_MAX_MEMORIZED_LINES) 216 self.wfile = socket._fileobject(self.request, 'wb', self.wbufsize) 217 218 def __init__(self, *args, **keywords): 219 self._request = _StandaloneRequest( 220 self, WebSocketRequestHandler.options.use_tls) 221 self._dispatcher = WebSocketRequestHandler.options.dispatcher 222 self._print_warnings_if_any() 223 self._handshaker = handshake.Handshaker( 224 self._request, self._dispatcher, 225 WebSocketRequestHandler.options.strict) 226 CGIHTTPServer.CGIHTTPRequestHandler.__init__( 227 self, *args, **keywords) 228 229 def _print_warnings_if_any(self): 230 warnings = self._dispatcher.source_warnings() 231 if warnings: 232 for warning in warnings: 233 logging.warning('mod_pywebsocket: %s' % warning) 234 235 def parse_request(self): 236 """Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request. 237 238 Return True to continue processing for HTTP(S), False otherwise. 239 """ 240 result = CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self) 241 if result: 242 try: 243 self._handshaker.do_handshake() 244 self._dispatcher.transfer_data(self._request) 245 return False 246 except handshake.HandshakeError, e: 247 # Handshake for ws(s) failed. Assume http(s). 248 logging.info('mod_pywebsocket: %s' % e) 249 return True 250 except dispatch.DispatchError, e: 251 logging.warning('mod_pywebsocket: %s' % e) 252 return False 253 except Exception, e: 254 logging.warning('mod_pywebsocket: %s' % e) 255 logging.info('mod_pywebsocket: %s' % util.get_stack_trace()) 256 return False 257 return result 258 259 def log_request(self, code='-', size='-'): 260 """Override BaseHTTPServer.log_request.""" 261 262 logging.info('"%s" %s %s', 263 self.requestline, str(code), str(size)) 264 265 def log_error(self, *args): 266 """Override BaseHTTPServer.log_error.""" 267 268 # Despite the name, this method is for warnings than for errors. 269 # For example, HTTP status code is logged by this method. 270 logging.warn('%s - %s' % (self.address_string(), (args[0] % args[1:]))) 271 272 def is_cgi(self): 273 """Test whether self.path corresponds to a CGI script. 274 275 Add extra check that self.path doesn't contains ..""" 276 if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self): 277 if '..' in self.path: 278 return False 279 return True 280 return False 281 282 283 def _configure_logging(options): 284 logger = logging.getLogger() 285 logger.setLevel(_LOG_LEVELS[options.log_level]) 286 if options.log_file: 287 handler = logging.handlers.RotatingFileHandler( 288 options.log_file, 'a', options.log_max, options.log_count) 289 else: 290 handler = logging.StreamHandler() 291 formatter = logging.Formatter( 292 "[%(asctime)s] [%(levelname)s] %(name)s: %(message)s") 293 handler.setFormatter(formatter) 294 logger.addHandler(handler) 295 296 def _alias_handlers(dispatcher, websock_handlers_map_file): 297 """Set aliases specified in websock_handler_map_file in dispatcher. 298 299 Args: 300 dispatcher: dispatch.Dispatcher instance 301 websock_handler_map_file: alias map file 302 """ 303 fp = open(websock_handlers_map_file) 304 try: 305 for line in fp: 306 if line[0] == '#' or line.isspace(): 307 continue 308 m = re.match('(\S+)\s+(\S+)', line) 309 if not m: 310 logging.warning('Wrong format in map file:' + line) 311 continue 312 try: 313 dispatcher.add_resource_path_alias( 314 m.group(1), m.group(2)) 315 except dispatch.DispatchError, e: 316 logging.error(str(e)) 317 finally: 318 fp.close() 319 320 321 322 def _main(): 323 parser = optparse.OptionParser() 324 parser.add_option('-p', '--port', dest='port', type='int', 325 default=handshake._DEFAULT_WEB_SOCKET_PORT, 326 help='port to listen to') 327 parser.add_option('-w', '--websock_handlers', dest='websock_handlers', 328 default='.', 329 help='Web Socket handlers root directory.') 330 parser.add_option('-m', '--websock_handlers_map_file', 331 dest='websock_handlers_map_file', 332 default=None, 333 help=('Web Socket handlers map file. ' 334 'Each line consists of alias_resource_path and ' 335 'existing_resource_path, separated by spaces.')) 336 parser.add_option('-s', '--scan_dir', dest='scan_dir', 337 default=None, 338 help=('Web Socket handlers scan directory. ' 339 'Must be a directory under websock_handlers.')) 340 parser.add_option('-d', '--document_root', dest='document_root', 341 default='.', 342 help='Document root directory.') 343 parser.add_option('-x', '--cgi_paths', dest='cgi_paths', 344 default=None, 345 help=('CGI paths relative to document_root.' 346 'Comma-separated. (e.g -x /cgi,/htbin) ' 347 'Files under document_root/cgi_path are handled ' 348 'as CGI programs. Must be executable.')) 349 parser.add_option('-t', '--tls', dest='use_tls', action='store_true', 350 default=False, help='use TLS (wss://)') 351 parser.add_option('-k', '--private_key', dest='private_key', 352 default='', help='TLS private key file.') 353 parser.add_option('-c', '--certificate', dest='certificate', 354 default='', help='TLS certificate file.') 355 parser.add_option('-l', '--log_file', dest='log_file', 356 default='', help='Log file.') 357 parser.add_option('--log_level', type='choice', dest='log_level', 358 default='warn', 359 choices=['debug', 'info', 'warn', 'error', 'critical'], 360 help='Log level.') 361 parser.add_option('--log_max', dest='log_max', type='int', 362 default=_DEFAULT_LOG_MAX_BYTES, 363 help='Log maximum bytes') 364 parser.add_option('--log_count', dest='log_count', type='int', 365 default=_DEFAULT_LOG_BACKUP_COUNT, 366 help='Log backup count') 367 parser.add_option('--strict', dest='strict', action='store_true', 368 default=False, help='Strictly check handshake request') 369 parser.add_option('-q', '--queue', dest='request_queue_size', type='int', 370 default=_DEFAULT_REQUEST_QUEUE_SIZE, 371 help='request queue size') 372 options = parser.parse_args()[0] 373 374 os.chdir(options.document_root) 375 376 _configure_logging(options) 377 378 SocketServer.TCPServer.request_queue_size = options.request_queue_size 379 CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = [] 380 381 if options.cgi_paths: 382 CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = \ 383 options.cgi_paths.split(',') 384 385 if options.use_tls: 386 if not _HAS_OPEN_SSL: 387 logging.critical('To use TLS, install pyOpenSSL.') 388 sys.exit(1) 389 if not options.private_key or not options.certificate: 390 logging.critical( 391 'To use TLS, specify private_key and certificate.') 392 sys.exit(1) 393 394 if not options.scan_dir: 395 options.scan_dir = options.websock_handlers 396 397 try: 398 # Share a Dispatcher among request handlers to save time for 399 # instantiation. Dispatcher can be shared because it is thread-safe. 400 options.dispatcher = dispatch.Dispatcher(options.websock_handlers, 401 options.scan_dir) 402 if options.websock_handlers_map_file: 403 _alias_handlers(options.dispatcher, 404 options.websock_handlers_map_file) 405 _print_warnings_if_any(options.dispatcher) 406 407 WebSocketRequestHandler.options = options 408 WebSocketServer.options = options 409 410 server = WebSocketServer(('', options.port), WebSocketRequestHandler) 411 server.serve_forever() 412 except Exception, e: 413 logging.critical(str(e)) 414 sys.exit(1) 415 416 417 if __name__ == '__main__': 418 _main() 419 420 421 # vi:sts=4 sw=4 et 422