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