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