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