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