1#!/usr/bin/env python 2# 3# Copyright 2012, 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 WebSocket server. 34 35BASIC USAGE 36 37Use this server to run mod_pywebsocket without Apache HTTP Server. 38 39Usage: 40 python standalone.py [-p <ws_port>] [-w <websock_handlers>] 41 [-s <scan_dir>] 42 [-d <document_root>] 43 [-m <websock_handlers_map_file>] 44 ... for other options, see _main below ... 45 46<ws_port> is the port number to use for ws:// connection. 47 48<document_root> is the path to the root directory of HTML files. 49 50<websock_handlers> is the path to the root directory of WebSocket handlers. 51See __init__.py for details of <websock_handlers> and how to write WebSocket 52handlers. If this path is relative, <document_root> is used as the base. 53 54<scan_dir> is a path under the root directory. If specified, only the 55handlers under scan_dir are scanned. This is useful in saving scan time. 56 57 58SUPPORTING TLS 59 60To support TLS, run standalone.py with -t, -k, and -c options. 61 62 63SUPPORTING CLIENT AUTHENTICATION 64 65To support client authentication with TLS, run standalone.py with -t, -k, -c, 66and --tls-client-auth, and --tls-client-ca options. 67 68E.g., $./standalone.py -d ../example -p 10443 -t -c ../test/cert/cert.pem -k 69../test/cert/key.pem --tls-client-auth --tls-client-ca=../test/cert/cacert.pem 70 71 72CONFIGURATION FILE 73 74You can also write a configuration file and use it by specifying the path to 75the configuration file by --config option. Please write a configuration file 76following the documentation of the Python ConfigParser library. Name of each 77entry must be the long version argument name. E.g. to set log level to debug, 78add the following line: 79 80log_level=debug 81 82For options which doesn't take value, please add some fake value. E.g. for 83--tls option, add the following line: 84 85tls=True 86 87Note that tls will be enabled even if you write tls=False as the value part is 88fake. 89 90When both a command line argument and a configuration file entry are set for 91the same configuration item, the command line value will override one in the 92configuration file. 93 94 95THREADING 96 97This server is derived from SocketServer.ThreadingMixIn. Hence a thread is 98used for each request. 99 100 101SECURITY WARNING 102 103This uses CGIHTTPServer and CGIHTTPServer is not secure. 104It may execute arbitrary Python code or external programs. It should not be 105used outside a firewall. 106""" 107 108import BaseHTTPServer 109import CGIHTTPServer 110import SimpleHTTPServer 111import SocketServer 112import ConfigParser 113import base64 114import httplib 115import logging 116import logging.handlers 117import optparse 118import os 119import re 120import select 121import socket 122import sys 123import threading 124import time 125 126_HAS_SSL = False 127_HAS_OPEN_SSL = False 128try: 129 import ssl 130 _HAS_SSL = True 131except ImportError: 132 try: 133 import OpenSSL.SSL 134 _HAS_OPEN_SSL = True 135 except ImportError: 136 pass 137 138from mod_pywebsocket import common 139from mod_pywebsocket import dispatch 140from mod_pywebsocket import handshake 141from mod_pywebsocket import http_header_util 142from mod_pywebsocket import memorizingfile 143from mod_pywebsocket import util 144 145 146_DEFAULT_LOG_MAX_BYTES = 1024 * 256 147_DEFAULT_LOG_BACKUP_COUNT = 5 148 149_DEFAULT_REQUEST_QUEUE_SIZE = 128 150 151# 1024 is practically large enough to contain WebSocket handshake lines. 152_MAX_MEMORIZED_LINES = 1024 153 154 155class _StandaloneConnection(object): 156 """Mimic mod_python mp_conn.""" 157 158 def __init__(self, request_handler): 159 """Construct an instance. 160 161 Args: 162 request_handler: A WebSocketRequestHandler instance. 163 """ 164 165 self._request_handler = request_handler 166 167 def get_local_addr(self): 168 """Getter to mimic mp_conn.local_addr.""" 169 170 return (self._request_handler.server.server_name, 171 self._request_handler.server.server_port) 172 local_addr = property(get_local_addr) 173 174 def get_remote_addr(self): 175 """Getter to mimic mp_conn.remote_addr. 176 177 Setting the property in __init__ won't work because the request 178 handler is not initialized yet there.""" 179 180 return self._request_handler.client_address 181 remote_addr = property(get_remote_addr) 182 183 def write(self, data): 184 """Mimic mp_conn.write().""" 185 186 return self._request_handler.wfile.write(data) 187 188 def read(self, length): 189 """Mimic mp_conn.read().""" 190 191 return self._request_handler.rfile.read(length) 192 193 def get_memorized_lines(self): 194 """Get memorized lines.""" 195 196 return self._request_handler.rfile.get_memorized_lines() 197 198 199class _StandaloneRequest(object): 200 """Mimic mod_python request.""" 201 202 def __init__(self, request_handler, use_tls): 203 """Construct an instance. 204 205 Args: 206 request_handler: A WebSocketRequestHandler instance. 207 """ 208 209 self._logger = util.get_class_logger(self) 210 211 self._request_handler = request_handler 212 self.connection = _StandaloneConnection(request_handler) 213 self._use_tls = use_tls 214 self.headers_in = request_handler.headers 215 216 def get_uri(self): 217 """Getter to mimic request.uri.""" 218 219 return self._request_handler.path 220 uri = property(get_uri) 221 222 def get_method(self): 223 """Getter to mimic request.method.""" 224 225 return self._request_handler.command 226 method = property(get_method) 227 228 def is_https(self): 229 """Mimic request.is_https().""" 230 231 return self._use_tls 232 233 def _drain_received_data(self): 234 """Don't use this method from WebSocket handler. Drains unread data 235 in the receive buffer. 236 """ 237 238 raw_socket = self._request_handler.connection 239 drained_data = util.drain_received_data(raw_socket) 240 241 if drained_data: 242 self._logger.debug( 243 'Drained data following close frame: %r', drained_data) 244 245 246class _StandaloneSSLConnection(object): 247 """A wrapper class for OpenSSL.SSL.Connection to provide makefile method 248 which is not supported by the class. 249 """ 250 251 def __init__(self, connection): 252 self._connection = connection 253 254 def __getattribute__(self, name): 255 if name in ('_connection', 'makefile'): 256 return object.__getattribute__(self, name) 257 return self._connection.__getattribute__(name) 258 259 def __setattr__(self, name, value): 260 if name in ('_connection', 'makefile'): 261 return object.__setattr__(self, name, value) 262 return self._connection.__setattr__(name, value) 263 264 def makefile(self, mode='r', bufsize=-1): 265 return socket._fileobject(self._connection, mode, bufsize) 266 267 268class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): 269 """HTTPServer specialized for WebSocket.""" 270 271 # Overrides SocketServer.ThreadingMixIn.daemon_threads 272 daemon_threads = True 273 # Overrides BaseHTTPServer.HTTPServer.allow_reuse_address 274 allow_reuse_address = True 275 276 def __init__(self, options): 277 """Override SocketServer.TCPServer.__init__ to set SSL enabled 278 socket object to self.socket before server_bind and server_activate, 279 if necessary. 280 """ 281 282 self._logger = util.get_class_logger(self) 283 284 self.request_queue_size = options.request_queue_size 285 self.__ws_is_shut_down = threading.Event() 286 self.__ws_serving = False 287 288 SocketServer.BaseServer.__init__( 289 self, (options.server_host, options.port), WebSocketRequestHandler) 290 291 # Expose the options object to allow handler objects access it. We name 292 # it with websocket_ prefix to avoid conflict. 293 self.websocket_server_options = options 294 295 self._create_sockets() 296 self.server_bind() 297 self.server_activate() 298 299 def _create_sockets(self): 300 self.server_name, self.server_port = self.server_address 301 self._sockets = [] 302 if not self.server_name: 303 # On platforms that doesn't support IPv6, the first bind fails. 304 # On platforms that supports IPv6 305 # - If it binds both IPv4 and IPv6 on call with AF_INET6, the 306 # first bind succeeds and the second fails (we'll see 'Address 307 # already in use' error). 308 # - If it binds only IPv6 on call with AF_INET6, both call are 309 # expected to succeed to listen both protocol. 310 addrinfo_array = [ 311 (socket.AF_INET6, socket.SOCK_STREAM, '', '', ''), 312 (socket.AF_INET, socket.SOCK_STREAM, '', '', '')] 313 else: 314 addrinfo_array = socket.getaddrinfo(self.server_name, 315 self.server_port, 316 socket.AF_UNSPEC, 317 socket.SOCK_STREAM, 318 socket.IPPROTO_TCP) 319 for addrinfo in addrinfo_array: 320 self._logger.info('Create socket on: %r', addrinfo) 321 family, socktype, proto, canonname, sockaddr = addrinfo 322 try: 323 socket_ = socket.socket(family, socktype) 324 except Exception, e: 325 self._logger.info('Skip by failure: %r', e) 326 continue 327 if self.websocket_server_options.use_tls: 328 if _HAS_SSL: 329 if self.websocket_server_options.tls_client_auth: 330 client_cert_ = ssl.CERT_REQUIRED 331 else: 332 client_cert_ = ssl.CERT_NONE 333 socket_ = ssl.wrap_socket(socket_, 334 keyfile=self.websocket_server_options.private_key, 335 certfile=self.websocket_server_options.certificate, 336 ssl_version=ssl.PROTOCOL_SSLv23, 337 ca_certs=self.websocket_server_options.tls_client_ca, 338 cert_reqs=client_cert_) 339 if _HAS_OPEN_SSL: 340 ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) 341 ctx.use_privatekey_file( 342 self.websocket_server_options.private_key) 343 ctx.use_certificate_file( 344 self.websocket_server_options.certificate) 345 socket_ = OpenSSL.SSL.Connection(ctx, socket_) 346 self._sockets.append((socket_, addrinfo)) 347 348 def server_bind(self): 349 """Override SocketServer.TCPServer.server_bind to enable multiple 350 sockets bind. 351 """ 352 353 failed_sockets = [] 354 355 for socketinfo in self._sockets: 356 socket_, addrinfo = socketinfo 357 self._logger.info('Bind on: %r', addrinfo) 358 if self.allow_reuse_address: 359 socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 360 try: 361 socket_.bind(self.server_address) 362 except Exception, e: 363 self._logger.info('Skip by failure: %r', e) 364 socket_.close() 365 failed_sockets.append(socketinfo) 366 367 for socketinfo in failed_sockets: 368 self._sockets.remove(socketinfo) 369 370 def server_activate(self): 371 """Override SocketServer.TCPServer.server_activate to enable multiple 372 sockets listen. 373 """ 374 375 failed_sockets = [] 376 377 for socketinfo in self._sockets: 378 socket_, addrinfo = socketinfo 379 self._logger.info('Listen on: %r', addrinfo) 380 try: 381 socket_.listen(self.request_queue_size) 382 except Exception, e: 383 self._logger.info('Skip by failure: %r', e) 384 socket_.close() 385 failed_sockets.append(socketinfo) 386 387 for socketinfo in failed_sockets: 388 self._sockets.remove(socketinfo) 389 390 if len(self._sockets) == 0: 391 self._logger.critical( 392 'No sockets activated. Use info log level to see the reason.') 393 394 def server_close(self): 395 """Override SocketServer.TCPServer.server_close to enable multiple 396 sockets close. 397 """ 398 399 for socketinfo in self._sockets: 400 socket_, addrinfo = socketinfo 401 self._logger.info('Close on: %r', addrinfo) 402 socket_.close() 403 404 def fileno(self): 405 """Override SocketServer.TCPServer.fileno.""" 406 407 self._logger.critical('Not supported: fileno') 408 return self._sockets[0][0].fileno() 409 410 def handle_error(self, rquest, client_address): 411 """Override SocketServer.handle_error.""" 412 413 self._logger.error( 414 'Exception in processing request from: %r\n%s', 415 client_address, 416 util.get_stack_trace()) 417 # Note: client_address is a tuple. 418 419 def get_request(self): 420 """Override TCPServer.get_request to wrap OpenSSL.SSL.Connection 421 object with _StandaloneSSLConnection to provide makefile method. We 422 cannot substitute OpenSSL.SSL.Connection.makefile since it's readonly 423 attribute. 424 """ 425 426 accepted_socket, client_address = self.socket.accept() 427 if self.websocket_server_options.use_tls and _HAS_OPEN_SSL: 428 accepted_socket = _StandaloneSSLConnection(accepted_socket) 429 return accepted_socket, client_address 430 431 def serve_forever(self, poll_interval=0.5): 432 """Override SocketServer.BaseServer.serve_forever.""" 433 434 self.__ws_serving = True 435 self.__ws_is_shut_down.clear() 436 handle_request = self.handle_request 437 if hasattr(self, '_handle_request_noblock'): 438 handle_request = self._handle_request_noblock 439 else: 440 self._logger.warning('Fallback to blocking request handler') 441 try: 442 while self.__ws_serving: 443 r, w, e = select.select( 444 [socket_[0] for socket_ in self._sockets], 445 [], [], poll_interval) 446 for socket_ in r: 447 self.socket = socket_ 448 handle_request() 449 self.socket = None 450 finally: 451 self.__ws_is_shut_down.set() 452 453 def shutdown(self): 454 """Override SocketServer.BaseServer.shutdown.""" 455 456 self.__ws_serving = False 457 self.__ws_is_shut_down.wait() 458 459 460class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): 461 """CGIHTTPRequestHandler specialized for WebSocket.""" 462 463 # Use httplib.HTTPMessage instead of mimetools.Message. 464 MessageClass = httplib.HTTPMessage 465 466 def setup(self): 467 """Override SocketServer.StreamRequestHandler.setup to wrap rfile 468 with MemorizingFile. 469 470 This method will be called by BaseRequestHandler's constructor 471 before calling BaseHTTPRequestHandler.handle. 472 BaseHTTPRequestHandler.handle will call 473 BaseHTTPRequestHandler.handle_one_request and it will call 474 WebSocketRequestHandler.parse_request. 475 """ 476 477 # Call superclass's setup to prepare rfile, wfile, etc. See setup 478 # definition on the root class SocketServer.StreamRequestHandler to 479 # understand what this does. 480 CGIHTTPServer.CGIHTTPRequestHandler.setup(self) 481 482 self.rfile = memorizingfile.MemorizingFile( 483 self.rfile, 484 max_memorized_lines=_MAX_MEMORIZED_LINES) 485 486 def __init__(self, request, client_address, server): 487 self._logger = util.get_class_logger(self) 488 489 self._options = server.websocket_server_options 490 491 # Overrides CGIHTTPServerRequestHandler.cgi_directories. 492 self.cgi_directories = self._options.cgi_directories 493 # Replace CGIHTTPRequestHandler.is_executable method. 494 if self._options.is_executable_method is not None: 495 self.is_executable = self._options.is_executable_method 496 497 # This actually calls BaseRequestHandler.__init__. 498 CGIHTTPServer.CGIHTTPRequestHandler.__init__( 499 self, request, client_address, server) 500 501 def parse_request(self): 502 """Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request. 503 504 Return True to continue processing for HTTP(S), False otherwise. 505 506 See BaseHTTPRequestHandler.handle_one_request method which calls 507 this method to understand how the return value will be handled. 508 """ 509 510 # We hook parse_request method, but also call the original 511 # CGIHTTPRequestHandler.parse_request since when we return False, 512 # CGIHTTPRequestHandler.handle_one_request continues processing and 513 # it needs variables set by CGIHTTPRequestHandler.parse_request. 514 # 515 # Variables set by this method will be also used by WebSocket request 516 # handling (self.path, self.command, self.requestline, etc. See also 517 # how _StandaloneRequest's members are implemented using these 518 # attributes). 519 if not CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self): 520 return False 521 522 if self._options.use_basic_auth: 523 auth = self.headers.getheader('Authorization') 524 if auth != self._options.basic_auth_credential: 525 self.send_response(401) 526 self.send_header('WWW-Authenticate', 527 'Basic realm="Pywebsocket"') 528 self.end_headers() 529 self._logger.info('Request basic authentication') 530 return True 531 532 host, port, resource = http_header_util.parse_uri(self.path) 533 if resource is None: 534 self._logger.info('Invalid URI: %r', self.path) 535 self._logger.info('Fallback to CGIHTTPRequestHandler') 536 return True 537 server_options = self.server.websocket_server_options 538 if host is not None: 539 validation_host = server_options.validation_host 540 if validation_host is not None and host != validation_host: 541 self._logger.info('Invalid host: %r (expected: %r)', 542 host, 543 validation_host) 544 self._logger.info('Fallback to CGIHTTPRequestHandler') 545 return True 546 if port is not None: 547 validation_port = server_options.validation_port 548 if validation_port is not None and port != validation_port: 549 self._logger.info('Invalid port: %r (expected: %r)', 550 port, 551 validation_port) 552 self._logger.info('Fallback to CGIHTTPRequestHandler') 553 return True 554 self.path = resource 555 556 request = _StandaloneRequest(self, self._options.use_tls) 557 558 try: 559 # Fallback to default http handler for request paths for which 560 # we don't have request handlers. 561 if not self._options.dispatcher.get_handler_suite(self.path): 562 self._logger.info('No handler for resource: %r', 563 self.path) 564 self._logger.info('Fallback to CGIHTTPRequestHandler') 565 return True 566 except dispatch.DispatchException, e: 567 self._logger.info('%s', e) 568 self.send_error(e.status) 569 return False 570 571 # If any Exceptions without except clause setup (including 572 # DispatchException) is raised below this point, it will be caught 573 # and logged by WebSocketServer. 574 575 try: 576 try: 577 handshake.do_handshake( 578 request, 579 self._options.dispatcher, 580 allowDraft75=self._options.allow_draft75, 581 strict=self._options.strict) 582 except handshake.VersionException, e: 583 self._logger.info('%s', e) 584 self.send_response(common.HTTP_STATUS_BAD_REQUEST) 585 self.send_header(common.SEC_WEBSOCKET_VERSION_HEADER, 586 e.supported_versions) 587 self.end_headers() 588 return False 589 except handshake.HandshakeException, e: 590 # Handshake for ws(s) failed. 591 self._logger.info('%s', e) 592 self.send_error(e.status) 593 return False 594 595 request._dispatcher = self._options.dispatcher 596 self._options.dispatcher.transfer_data(request) 597 except handshake.AbortedByUserException, e: 598 self._logger.info('%s', e) 599 return False 600 601 def log_request(self, code='-', size='-'): 602 """Override BaseHTTPServer.log_request.""" 603 604 self._logger.info('"%s" %s %s', 605 self.requestline, str(code), str(size)) 606 607 def log_error(self, *args): 608 """Override BaseHTTPServer.log_error.""" 609 610 # Despite the name, this method is for warnings than for errors. 611 # For example, HTTP status code is logged by this method. 612 self._logger.warning('%s - %s', 613 self.address_string(), 614 args[0] % args[1:]) 615 616 def is_cgi(self): 617 """Test whether self.path corresponds to a CGI script. 618 619 Add extra check that self.path doesn't contains .. 620 Also check if the file is a executable file or not. 621 If the file is not executable, it is handled as static file or dir 622 rather than a CGI script. 623 """ 624 625 if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self): 626 if '..' in self.path: 627 return False 628 # strip query parameter from request path 629 resource_name = self.path.split('?', 2)[0] 630 # convert resource_name into real path name in filesystem. 631 scriptfile = self.translate_path(resource_name) 632 if not os.path.isfile(scriptfile): 633 return False 634 if not self.is_executable(scriptfile): 635 return False 636 return True 637 return False 638 639 640def _get_logger_from_class(c): 641 return logging.getLogger('%s.%s' % (c.__module__, c.__name__)) 642 643 644def _configure_logging(options): 645 logging.addLevelName(common.LOGLEVEL_FINE, 'FINE') 646 647 logger = logging.getLogger() 648 logger.setLevel(logging.getLevelName(options.log_level.upper())) 649 if options.log_file: 650 handler = logging.handlers.RotatingFileHandler( 651 options.log_file, 'a', options.log_max, options.log_count) 652 else: 653 handler = logging.StreamHandler() 654 formatter = logging.Formatter( 655 '[%(asctime)s] [%(levelname)s] %(name)s: %(message)s') 656 handler.setFormatter(formatter) 657 logger.addHandler(handler) 658 659 deflate_log_level_name = logging.getLevelName( 660 options.deflate_log_level.upper()) 661 _get_logger_from_class(util._Deflater).setLevel( 662 deflate_log_level_name) 663 _get_logger_from_class(util._Inflater).setLevel( 664 deflate_log_level_name) 665 666 667def _alias_handlers(dispatcher, websock_handlers_map_file): 668 """Set aliases specified in websock_handler_map_file in dispatcher. 669 670 Args: 671 dispatcher: dispatch.Dispatcher instance 672 websock_handler_map_file: alias map file 673 """ 674 675 fp = open(websock_handlers_map_file) 676 try: 677 for line in fp: 678 if line[0] == '#' or line.isspace(): 679 continue 680 m = re.match('(\S+)\s+(\S+)', line) 681 if not m: 682 logging.warning('Wrong format in map file:' + line) 683 continue 684 try: 685 dispatcher.add_resource_path_alias( 686 m.group(1), m.group(2)) 687 except dispatch.DispatchException, e: 688 logging.error(str(e)) 689 finally: 690 fp.close() 691 692 693def _build_option_parser(): 694 parser = optparse.OptionParser() 695 696 parser.add_option('--config', dest='config_file', type='string', 697 default=None, 698 help=('Path to configuration file. See the file comment ' 699 'at the top of this file for the configuration ' 700 'file format')) 701 parser.add_option('-H', '--server-host', '--server_host', 702 dest='server_host', 703 default='', 704 help='server hostname to listen to') 705 parser.add_option('-V', '--validation-host', '--validation_host', 706 dest='validation_host', 707 default=None, 708 help='server hostname to validate in absolute path.') 709 parser.add_option('-p', '--port', dest='port', type='int', 710 default=common.DEFAULT_WEB_SOCKET_PORT, 711 help='port to listen to') 712 parser.add_option('-P', '--validation-port', '--validation_port', 713 dest='validation_port', type='int', 714 default=None, 715 help='server port to validate in absolute path.') 716 parser.add_option('-w', '--websock-handlers', '--websock_handlers', 717 dest='websock_handlers', 718 default='.', 719 help='WebSocket handlers root directory.') 720 parser.add_option('-m', '--websock-handlers-map-file', 721 '--websock_handlers_map_file', 722 dest='websock_handlers_map_file', 723 default=None, 724 help=('WebSocket handlers map file. ' 725 'Each line consists of alias_resource_path and ' 726 'existing_resource_path, separated by spaces.')) 727 parser.add_option('-s', '--scan-dir', '--scan_dir', dest='scan_dir', 728 default=None, 729 help=('WebSocket handlers scan directory. ' 730 'Must be a directory under websock_handlers.')) 731 parser.add_option('--allow-handlers-outside-root-dir', 732 '--allow_handlers_outside_root_dir', 733 dest='allow_handlers_outside_root_dir', 734 action='store_true', 735 default=False, 736 help=('Scans WebSocket handlers even if their canonical ' 737 'path is not under websock_handlers.')) 738 parser.add_option('-d', '--document-root', '--document_root', 739 dest='document_root', default='.', 740 help='Document root directory.') 741 parser.add_option('-x', '--cgi-paths', '--cgi_paths', dest='cgi_paths', 742 default=None, 743 help=('CGI paths relative to document_root.' 744 'Comma-separated. (e.g -x /cgi,/htbin) ' 745 'Files under document_root/cgi_path are handled ' 746 'as CGI programs. Must be executable.')) 747 parser.add_option('-t', '--tls', dest='use_tls', action='store_true', 748 default=False, help='use TLS (wss://)') 749 parser.add_option('-k', '--private-key', '--private_key', 750 dest='private_key', 751 default='', help='TLS private key file.') 752 parser.add_option('-c', '--certificate', dest='certificate', 753 default='', help='TLS certificate file.') 754 parser.add_option('--tls-client-auth', dest='tls_client_auth', 755 action='store_true', default=False, 756 help='Requires TLS client auth on every connection.') 757 parser.add_option('--tls-client-ca', dest='tls_client_ca', default='', 758 help=('Specifies a pem file which contains a set of ' 759 'concatenated CA certificates which are used to ' 760 'validate certificates passed from clients')) 761 parser.add_option('--basic-auth', dest='use_basic_auth', 762 action='store_true', default=False, 763 help='Requires Basic authentication.') 764 parser.add_option('--basic-auth-credential', 765 dest='basic_auth_credential', default='test:test', 766 help='Specifies the credential of basic authentication ' 767 'by username:password pair (e.g. test:test).') 768 parser.add_option('-l', '--log-file', '--log_file', dest='log_file', 769 default='', help='Log file.') 770 # Custom log level: 771 # - FINE: Prints status of each frame processing step 772 parser.add_option('--log-level', '--log_level', type='choice', 773 dest='log_level', default='warn', 774 choices=['fine', 775 'debug', 'info', 'warning', 'warn', 'error', 776 'critical'], 777 help='Log level.') 778 parser.add_option('--deflate-log-level', '--deflate_log_level', 779 type='choice', 780 dest='deflate_log_level', default='warn', 781 choices=['debug', 'info', 'warning', 'warn', 'error', 782 'critical'], 783 help='Log level for _Deflater and _Inflater.') 784 parser.add_option('--thread-monitor-interval-in-sec', 785 '--thread_monitor_interval_in_sec', 786 dest='thread_monitor_interval_in_sec', 787 type='int', default=-1, 788 help=('If positive integer is specified, run a thread ' 789 'monitor to show the status of server threads ' 790 'periodically in the specified inteval in ' 791 'second. If non-positive integer is specified, ' 792 'disable the thread monitor.')) 793 parser.add_option('--log-max', '--log_max', dest='log_max', type='int', 794 default=_DEFAULT_LOG_MAX_BYTES, 795 help='Log maximum bytes') 796 parser.add_option('--log-count', '--log_count', dest='log_count', 797 type='int', default=_DEFAULT_LOG_BACKUP_COUNT, 798 help='Log backup count') 799 parser.add_option('--allow-draft75', dest='allow_draft75', 800 action='store_true', default=False, 801 help='Allow draft 75 handshake') 802 parser.add_option('--strict', dest='strict', action='store_true', 803 default=False, help='Strictly check handshake request') 804 parser.add_option('-q', '--queue', dest='request_queue_size', type='int', 805 default=_DEFAULT_REQUEST_QUEUE_SIZE, 806 help='request queue size') 807 808 return parser 809 810 811class ThreadMonitor(threading.Thread): 812 daemon = True 813 814 def __init__(self, interval_in_sec): 815 threading.Thread.__init__(self, name='ThreadMonitor') 816 817 self._logger = util.get_class_logger(self) 818 819 self._interval_in_sec = interval_in_sec 820 821 def run(self): 822 while True: 823 thread_name_list = [] 824 for thread in threading.enumerate(): 825 thread_name_list.append(thread.name) 826 self._logger.info( 827 "%d active threads: %s", 828 threading.active_count(), 829 ', '.join(thread_name_list)) 830 time.sleep(self._interval_in_sec) 831 832 833def _parse_args_and_config(args): 834 parser = _build_option_parser() 835 836 # First, parse options without configuration file. 837 temporary_options, temporary_args = parser.parse_args(args=args) 838 if temporary_args: 839 logging.critical( 840 'Unrecognized positional arguments: %r', temporary_args) 841 sys.exit(1) 842 843 if temporary_options.config_file: 844 try: 845 config_fp = open(temporary_options.config_file, 'r') 846 except IOError, e: 847 logging.critical( 848 'Failed to open configuration file %r: %r', 849 temporary_options.config_file, 850 e) 851 sys.exit(1) 852 853 config_parser = ConfigParser.SafeConfigParser() 854 config_parser.readfp(config_fp) 855 config_fp.close() 856 857 args_from_config = [] 858 for name, value in config_parser.items('pywebsocket'): 859 args_from_config.append('--' + name) 860 args_from_config.append(value) 861 if args is None: 862 args = args_from_config 863 else: 864 args = args_from_config + args 865 return parser.parse_args(args=args) 866 else: 867 return temporary_options, temporary_args 868 869 870def _main(args=None): 871 options, args = _parse_args_and_config(args=args) 872 873 os.chdir(options.document_root) 874 875 _configure_logging(options) 876 877 # TODO(tyoshino): Clean up initialization of CGI related values. Move some 878 # of code here to WebSocketRequestHandler class if it's better. 879 options.cgi_directories = [] 880 options.is_executable_method = None 881 if options.cgi_paths: 882 options.cgi_directories = options.cgi_paths.split(',') 883 if sys.platform in ('cygwin', 'win32'): 884 cygwin_path = None 885 # For Win32 Python, it is expected that CYGWIN_PATH 886 # is set to a directory of cygwin binaries. 887 # For example, websocket_server.py in Chromium sets CYGWIN_PATH to 888 # full path of third_party/cygwin/bin. 889 if 'CYGWIN_PATH' in os.environ: 890 cygwin_path = os.environ['CYGWIN_PATH'] 891 util.wrap_popen3_for_win(cygwin_path) 892 893 def __check_script(scriptpath): 894 return util.get_script_interp(scriptpath, cygwin_path) 895 896 options.is_executable_method = __check_script 897 898 if options.use_tls: 899 if not (_HAS_SSL or _HAS_OPEN_SSL): 900 logging.critical('TLS support requires ssl or pyOpenSSL module.') 901 sys.exit(1) 902 if not options.private_key or not options.certificate: 903 logging.critical( 904 'To use TLS, specify private_key and certificate.') 905 sys.exit(1) 906 907 if options.tls_client_auth: 908 if not options.use_tls: 909 logging.critical('TLS must be enabled for client authentication.') 910 sys.exit(1) 911 if not _HAS_SSL: 912 logging.critical('Client authentication requires ssl module.') 913 914 if not options.scan_dir: 915 options.scan_dir = options.websock_handlers 916 917 if options.use_basic_auth: 918 options.basic_auth_credential = 'Basic ' + base64.b64encode( 919 options.basic_auth_credential) 920 921 try: 922 if options.thread_monitor_interval_in_sec > 0: 923 # Run a thread monitor to show the status of server threads for 924 # debugging. 925 ThreadMonitor(options.thread_monitor_interval_in_sec).start() 926 927 # Share a Dispatcher among request handlers to save time for 928 # instantiation. Dispatcher can be shared because it is thread-safe. 929 options.dispatcher = dispatch.Dispatcher( 930 options.websock_handlers, 931 options.scan_dir, 932 options.allow_handlers_outside_root_dir) 933 if options.websock_handlers_map_file: 934 _alias_handlers(options.dispatcher, 935 options.websock_handlers_map_file) 936 warnings = options.dispatcher.source_warnings() 937 if warnings: 938 for warning in warnings: 939 logging.warning('mod_pywebsocket: %s' % warning) 940 941 server = WebSocketServer(options) 942 server.serve_forever() 943 except Exception, e: 944 logging.critical('mod_pywebsocket: %s' % e) 945 logging.critical('mod_pywebsocket: %s' % util.get_stack_trace()) 946 sys.exit(1) 947 948 949if __name__ == '__main__': 950 _main(sys.argv[1:]) 951 952 953# vi:sts=4 sw=4 et 954