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