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