Home | History | Annotate | Download | only in mod_pywebsocket
      1 #!/usr/bin/env python
      2 #
      3 # Copyright 2009, 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 Web Socket server.
     34 
     35 Use this server to run mod_pywebsocket without Apache HTTP Server.
     36 
     37 Usage:
     38     python standalone.py [-p <ws_port>] [-w <websock_handlers>]
     39                          [-s <scan_dir>]
     40                          [-d <document_root>]
     41                          [-m <websock_handlers_map_file>]
     42                          ... for other options, see _main below ...
     43 
     44 <ws_port> is the port number to use for ws:// connection.
     45 
     46 <document_root> is the path to the root directory of HTML files.
     47 
     48 <websock_handlers> is the path to the root directory of Web Socket handlers.
     49 See __init__.py for details of <websock_handlers> and how to write Web Socket
     50 handlers. If this path is relative, <document_root> is used as the base.
     51 
     52 <scan_dir> is a path under the root directory. If specified, only the handlers
     53 under scan_dir are scanned. This is useful in saving scan time.
     54 
     55 Note:
     56 This server is derived from SocketServer.ThreadingMixIn. Hence a thread is
     57 used for each request.
     58 """
     59 
     60 import BaseHTTPServer
     61 import CGIHTTPServer
     62 import SimpleHTTPServer
     63 import SocketServer
     64 import logging
     65 import logging.handlers
     66 import optparse
     67 import os
     68 import re
     69 import socket
     70 import sys
     71 
     72 _HAS_OPEN_SSL = False
     73 try:
     74     import OpenSSL.SSL
     75     _HAS_OPEN_SSL = True
     76 except ImportError:
     77     pass
     78 
     79 import dispatch
     80 import handshake
     81 import memorizingfile
     82 import util
     83 
     84 
     85 _LOG_LEVELS = {
     86     'debug': logging.DEBUG,
     87     'info': logging.INFO,
     88     'warn': logging.WARN,
     89     'error': logging.ERROR,
     90     'critical': logging.CRITICAL};
     91 
     92 _DEFAULT_LOG_MAX_BYTES = 1024 * 256
     93 _DEFAULT_LOG_BACKUP_COUNT = 5
     94 
     95 _DEFAULT_REQUEST_QUEUE_SIZE = 128
     96 
     97 # 1024 is practically large enough to contain WebSocket handshake lines.
     98 _MAX_MEMORIZED_LINES = 1024
     99 
    100 def _print_warnings_if_any(dispatcher):
    101     warnings = dispatcher.source_warnings()
    102     if warnings:
    103         for warning in warnings:
    104             logging.warning('mod_pywebsocket: %s' % warning)
    105 
    106 
    107 class _StandaloneConnection(object):
    108     """Mimic mod_python mp_conn."""
    109 
    110     def __init__(self, request_handler):
    111         """Construct an instance.
    112 
    113         Args:
    114             request_handler: A WebSocketRequestHandler instance.
    115         """
    116         self._request_handler = request_handler
    117 
    118     def get_local_addr(self):
    119         """Getter to mimic mp_conn.local_addr."""
    120         return (self._request_handler.server.server_name,
    121                 self._request_handler.server.server_port)
    122     local_addr = property(get_local_addr)
    123 
    124     def get_remote_addr(self):
    125         """Getter to mimic mp_conn.remote_addr.
    126 
    127         Setting the property in __init__ won't work because the request
    128         handler is not initialized yet there."""
    129         return self._request_handler.client_address
    130     remote_addr = property(get_remote_addr)
    131 
    132     def write(self, data):
    133         """Mimic mp_conn.write()."""
    134         return self._request_handler.wfile.write(data)
    135 
    136     def read(self, length):
    137         """Mimic mp_conn.read()."""
    138         return self._request_handler.rfile.read(length)
    139 
    140     def get_memorized_lines(self):
    141         """Get memorized lines."""
    142         return self._request_handler.rfile.get_memorized_lines()
    143 
    144 
    145 class _StandaloneRequest(object):
    146     """Mimic mod_python request."""
    147 
    148     def __init__(self, request_handler, use_tls):
    149         """Construct an instance.
    150 
    151         Args:
    152             request_handler: A WebSocketRequestHandler instance.
    153         """
    154         self._request_handler = request_handler
    155         self.connection = _StandaloneConnection(request_handler)
    156         self._use_tls = use_tls
    157 
    158     def get_uri(self):
    159         """Getter to mimic request.uri."""
    160         return self._request_handler.path
    161     uri = property(get_uri)
    162 
    163     def get_headers_in(self):
    164         """Getter to mimic request.headers_in."""
    165         return self._request_handler.headers
    166     headers_in = property(get_headers_in)
    167 
    168     def is_https(self):
    169         """Mimic request.is_https()."""
    170         return self._use_tls
    171 
    172 
    173 class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
    174     """HTTPServer specialized for Web Socket."""
    175 
    176     SocketServer.ThreadingMixIn.daemon_threads = True
    177 
    178     def __init__(self, server_address, RequestHandlerClass):
    179         """Override SocketServer.BaseServer.__init__."""
    180 
    181         SocketServer.BaseServer.__init__(
    182                 self, server_address, RequestHandlerClass)
    183         self.socket = self._create_socket()
    184         self.server_bind()
    185         self.server_activate()
    186 
    187     def _create_socket(self):
    188         socket_ = socket.socket(self.address_family, self.socket_type)
    189         if WebSocketServer.options.use_tls:
    190             ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
    191             ctx.use_privatekey_file(WebSocketServer.options.private_key)
    192             ctx.use_certificate_file(WebSocketServer.options.certificate)
    193             socket_ = OpenSSL.SSL.Connection(ctx, socket_)
    194         return socket_
    195 
    196     def handle_error(self, rquest, client_address):
    197         """Override SocketServer.handle_error."""
    198 
    199         logging.error(
    200             ('Exception in processing request from: %r' % (client_address,)) +
    201             '\n' + util.get_stack_trace())
    202         # Note: client_address is a tuple. To match it against %r, we need the
    203         # trailing comma.
    204 
    205 
    206 class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
    207     """CGIHTTPRequestHandler specialized for Web Socket."""
    208 
    209     def setup(self):
    210         """Override SocketServer.StreamRequestHandler.setup."""
    211 
    212         self.connection = self.request
    213         self.rfile = memorizingfile.MemorizingFile(
    214                 socket._fileobject(self.request, 'rb', self.rbufsize),
    215                 max_memorized_lines=_MAX_MEMORIZED_LINES)
    216         self.wfile = socket._fileobject(self.request, 'wb', self.wbufsize)
    217 
    218     def __init__(self, *args, **keywords):
    219         self._request = _StandaloneRequest(
    220                 self, WebSocketRequestHandler.options.use_tls)
    221         self._dispatcher = WebSocketRequestHandler.options.dispatcher
    222         self._print_warnings_if_any()
    223         self._handshaker = handshake.Handshaker(
    224                 self._request, self._dispatcher,
    225                 WebSocketRequestHandler.options.strict)
    226         CGIHTTPServer.CGIHTTPRequestHandler.__init__(
    227                 self, *args, **keywords)
    228 
    229     def _print_warnings_if_any(self):
    230         warnings = self._dispatcher.source_warnings()
    231         if warnings:
    232             for warning in warnings:
    233                 logging.warning('mod_pywebsocket: %s' % warning)
    234 
    235     def parse_request(self):
    236         """Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request.
    237 
    238         Return True to continue processing for HTTP(S), False otherwise.
    239         """
    240         result = CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self)
    241         if result:
    242             try:
    243                 self._handshaker.do_handshake()
    244                 self._dispatcher.transfer_data(self._request)
    245                 return False
    246             except handshake.HandshakeError, e:
    247                 # Handshake for ws(s) failed. Assume http(s).
    248                 logging.info('mod_pywebsocket: %s' % e)
    249                 return True
    250             except dispatch.DispatchError, e:
    251                 logging.warning('mod_pywebsocket: %s' % e)
    252                 return False
    253             except Exception, e:
    254                 logging.warning('mod_pywebsocket: %s' % e)
    255                 logging.info('mod_pywebsocket: %s' % util.get_stack_trace())
    256                 return False
    257         return result
    258 
    259     def log_request(self, code='-', size='-'):
    260         """Override BaseHTTPServer.log_request."""
    261 
    262         logging.info('"%s" %s %s',
    263                      self.requestline, str(code), str(size))
    264 
    265     def log_error(self, *args):
    266         """Override BaseHTTPServer.log_error."""
    267 
    268         # Despite the name, this method is for warnings than for errors.
    269         # For example, HTTP status code is logged by this method.
    270         logging.warn('%s - %s' % (self.address_string(), (args[0] % args[1:])))
    271 
    272     def is_cgi(self):
    273         """Test whether self.path corresponds to a CGI script.
    274 
    275         Add extra check that self.path doesn't contains .."""
    276         if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self):
    277             if '..' in self.path:
    278                 return False
    279             return True
    280         return False
    281 
    282 
    283 def _configure_logging(options):
    284     logger = logging.getLogger()
    285     logger.setLevel(_LOG_LEVELS[options.log_level])
    286     if options.log_file:
    287         handler = logging.handlers.RotatingFileHandler(
    288                 options.log_file, 'a', options.log_max, options.log_count)
    289     else:
    290         handler = logging.StreamHandler()
    291     formatter = logging.Formatter(
    292             "[%(asctime)s] [%(levelname)s] %(name)s: %(message)s")
    293     handler.setFormatter(formatter)
    294     logger.addHandler(handler)
    295 
    296 def _alias_handlers(dispatcher, websock_handlers_map_file):
    297     """Set aliases specified in websock_handler_map_file in dispatcher.
    298 
    299     Args:
    300         dispatcher: dispatch.Dispatcher instance
    301         websock_handler_map_file: alias map file
    302     """
    303     fp = open(websock_handlers_map_file)
    304     try:
    305         for line in fp:
    306             if line[0] == '#' or line.isspace():
    307                 continue
    308             m = re.match('(\S+)\s+(\S+)', line)
    309             if not m:
    310                 logging.warning('Wrong format in map file:' + line)
    311                 continue
    312             try:
    313                 dispatcher.add_resource_path_alias(
    314                     m.group(1), m.group(2))
    315             except dispatch.DispatchError, e:
    316                 logging.error(str(e))
    317     finally:
    318         fp.close()
    319 
    320 
    321 
    322 def _main():
    323     parser = optparse.OptionParser()
    324     parser.add_option('-p', '--port', dest='port', type='int',
    325                       default=handshake._DEFAULT_WEB_SOCKET_PORT,
    326                       help='port to listen to')
    327     parser.add_option('-w', '--websock_handlers', dest='websock_handlers',
    328                       default='.',
    329                       help='Web Socket handlers root directory.')
    330     parser.add_option('-m', '--websock_handlers_map_file',
    331                       dest='websock_handlers_map_file',
    332                       default=None,
    333                       help=('Web Socket handlers map file. '
    334                             'Each line consists of alias_resource_path and '
    335                             'existing_resource_path, separated by spaces.'))
    336     parser.add_option('-s', '--scan_dir', dest='scan_dir',
    337                       default=None,
    338                       help=('Web Socket handlers scan directory. '
    339                             'Must be a directory under websock_handlers.'))
    340     parser.add_option('-d', '--document_root', dest='document_root',
    341                       default='.',
    342                       help='Document root directory.')
    343     parser.add_option('-x', '--cgi_paths', dest='cgi_paths',
    344                       default=None,
    345                       help=('CGI paths relative to document_root.'
    346                             'Comma-separated. (e.g -x /cgi,/htbin) '
    347                             'Files under document_root/cgi_path are handled '
    348                             'as CGI programs. Must be executable.'))
    349     parser.add_option('-t', '--tls', dest='use_tls', action='store_true',
    350                       default=False, help='use TLS (wss://)')
    351     parser.add_option('-k', '--private_key', dest='private_key',
    352                       default='', help='TLS private key file.')
    353     parser.add_option('-c', '--certificate', dest='certificate',
    354                       default='', help='TLS certificate file.')
    355     parser.add_option('-l', '--log_file', dest='log_file',
    356                       default='', help='Log file.')
    357     parser.add_option('--log_level', type='choice', dest='log_level',
    358                       default='warn',
    359                       choices=['debug', 'info', 'warn', 'error', 'critical'],
    360                       help='Log level.')
    361     parser.add_option('--log_max', dest='log_max', type='int',
    362                       default=_DEFAULT_LOG_MAX_BYTES,
    363                       help='Log maximum bytes')
    364     parser.add_option('--log_count', dest='log_count', type='int',
    365                       default=_DEFAULT_LOG_BACKUP_COUNT,
    366                       help='Log backup count')
    367     parser.add_option('--strict', dest='strict', action='store_true',
    368                       default=False, help='Strictly check handshake request')
    369     parser.add_option('-q', '--queue', dest='request_queue_size', type='int',
    370                       default=_DEFAULT_REQUEST_QUEUE_SIZE,
    371                       help='request queue size')
    372     options = parser.parse_args()[0]
    373 
    374     os.chdir(options.document_root)
    375 
    376     _configure_logging(options)
    377 
    378     SocketServer.TCPServer.request_queue_size = options.request_queue_size
    379     CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = []
    380 
    381     if options.cgi_paths:
    382         CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = \
    383             options.cgi_paths.split(',')
    384 
    385     if options.use_tls:
    386         if not _HAS_OPEN_SSL:
    387             logging.critical('To use TLS, install pyOpenSSL.')
    388             sys.exit(1)
    389         if not options.private_key or not options.certificate:
    390             logging.critical(
    391                     'To use TLS, specify private_key and certificate.')
    392             sys.exit(1)
    393 
    394     if not options.scan_dir:
    395         options.scan_dir = options.websock_handlers
    396 
    397     try:
    398         # Share a Dispatcher among request handlers to save time for
    399         # instantiation.  Dispatcher can be shared because it is thread-safe.
    400         options.dispatcher = dispatch.Dispatcher(options.websock_handlers,
    401                                                  options.scan_dir)
    402         if options.websock_handlers_map_file:
    403             _alias_handlers(options.dispatcher,
    404                             options.websock_handlers_map_file)
    405         _print_warnings_if_any(options.dispatcher)
    406 
    407         WebSocketRequestHandler.options = options
    408         WebSocketServer.options = options
    409 
    410         server = WebSocketServer(('', options.port), WebSocketRequestHandler)
    411         server.serve_forever()
    412     except Exception, e:
    413         logging.critical(str(e))
    414         sys.exit(1)
    415 
    416 
    417 if __name__ == '__main__':
    418     _main()
    419 
    420 
    421 # vi:sts=4 sw=4 et
    422