testing/mochitest/pywebsocket/standalone.py

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/testing/mochitest/pywebsocket/standalone.py	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,870 @@
     1.4 +#!/usr/bin/env python
     1.5 +#
     1.6 +# Copyright 2011, Google Inc.
     1.7 +# All rights reserved.
     1.8 +#
     1.9 +# Redistribution and use in source and binary forms, with or without
    1.10 +# modification, are permitted provided that the following conditions are
    1.11 +# met:
    1.12 +#
    1.13 +#     * Redistributions of source code must retain the above copyright
    1.14 +# notice, this list of conditions and the following disclaimer.
    1.15 +#     * Redistributions in binary form must reproduce the above
    1.16 +# copyright notice, this list of conditions and the following disclaimer
    1.17 +# in the documentation and/or other materials provided with the
    1.18 +# distribution.
    1.19 +#     * Neither the name of Google Inc. nor the names of its
    1.20 +# contributors may be used to endorse or promote products derived from
    1.21 +# this software without specific prior written permission.
    1.22 +#
    1.23 +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    1.24 +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    1.25 +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
    1.26 +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
    1.27 +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    1.28 +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
    1.29 +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    1.30 +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
    1.31 +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    1.32 +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    1.33 +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    1.34 +
    1.35 +
    1.36 +"""Standalone WebSocket server.
    1.37 +
    1.38 +BASIC USAGE
    1.39 +
    1.40 +Use this server to run mod_pywebsocket without Apache HTTP Server.
    1.41 +
    1.42 +Usage:
    1.43 +    python standalone.py [-p <ws_port>] [-w <websock_handlers>]
    1.44 +                         [-s <scan_dir>]
    1.45 +                         [-d <document_root>]
    1.46 +                         [-m <websock_handlers_map_file>]
    1.47 +                         ... for other options, see _main below ...
    1.48 +
    1.49 +<ws_port> is the port number to use for ws:// connection.
    1.50 +
    1.51 +<document_root> is the path to the root directory of HTML files.
    1.52 +
    1.53 +<websock_handlers> is the path to the root directory of WebSocket handlers.
    1.54 +See __init__.py for details of <websock_handlers> and how to write WebSocket
    1.55 +handlers. If this path is relative, <document_root> is used as the base.
    1.56 +
    1.57 +<scan_dir> is a path under the root directory. If specified, only the
    1.58 +handlers under scan_dir are scanned. This is useful in saving scan time.
    1.59 +
    1.60 +
    1.61 +CONFIGURATION FILE
    1.62 +
    1.63 +You can also write a configuration file and use it by specifying the path to
    1.64 +the configuration file by --config option. Please write a configuration file
    1.65 +following the documentation of the Python ConfigParser library. Name of each
    1.66 +entry must be the long version argument name. E.g. to set log level to debug,
    1.67 +add the following line:
    1.68 +
    1.69 +log_level=debug
    1.70 +
    1.71 +For options which doesn't take value, please add some fake value. E.g. for
    1.72 +--tls option, add the following line:
    1.73 +
    1.74 +tls=True
    1.75 +
    1.76 +Note that tls will be enabled even if you write tls=False as the value part is
    1.77 +fake.
    1.78 +
    1.79 +When both a command line argument and a configuration file entry are set for
    1.80 +the same configuration item, the command line value will override one in the
    1.81 +configuration file.
    1.82 +
    1.83 +
    1.84 +THREADING
    1.85 +
    1.86 +This server is derived from SocketServer.ThreadingMixIn. Hence a thread is
    1.87 +used for each request.
    1.88 +
    1.89 +
    1.90 +SECURITY WARNING
    1.91 +
    1.92 +This uses CGIHTTPServer and CGIHTTPServer is not secure.
    1.93 +It may execute arbitrary Python code or external programs. It should not be
    1.94 +used outside a firewall.
    1.95 +"""
    1.96 +
    1.97 +import BaseHTTPServer
    1.98 +import CGIHTTPServer
    1.99 +import SimpleHTTPServer
   1.100 +import SocketServer
   1.101 +import ConfigParser
   1.102 +import httplib
   1.103 +import logging
   1.104 +import logging.handlers
   1.105 +import optparse
   1.106 +import os
   1.107 +import re
   1.108 +import select
   1.109 +import socket
   1.110 +import sys
   1.111 +import threading
   1.112 +import time
   1.113 +
   1.114 +_HAS_SSL = False
   1.115 +_HAS_OPEN_SSL = False
   1.116 +try:
   1.117 +    import ssl
   1.118 +    _HAS_SSL = True
   1.119 +except ImportError:
   1.120 +    try:
   1.121 +        import OpenSSL.SSL
   1.122 +        _HAS_OPEN_SSL = True
   1.123 +    except ImportError:
   1.124 +        pass
   1.125 +
   1.126 +from mod_pywebsocket import common
   1.127 +from mod_pywebsocket import dispatch
   1.128 +from mod_pywebsocket import handshake
   1.129 +from mod_pywebsocket import http_header_util
   1.130 +from mod_pywebsocket import memorizingfile
   1.131 +from mod_pywebsocket import util
   1.132 +
   1.133 +
   1.134 +_DEFAULT_LOG_MAX_BYTES = 1024 * 256
   1.135 +_DEFAULT_LOG_BACKUP_COUNT = 5
   1.136 +
   1.137 +_DEFAULT_REQUEST_QUEUE_SIZE = 128
   1.138 +
   1.139 +# 1024 is practically large enough to contain WebSocket handshake lines.
   1.140 +_MAX_MEMORIZED_LINES = 1024
   1.141 +
   1.142 +
   1.143 +class _StandaloneConnection(object):
   1.144 +    """Mimic mod_python mp_conn."""
   1.145 +
   1.146 +    def __init__(self, request_handler):
   1.147 +        """Construct an instance.
   1.148 +
   1.149 +        Args:
   1.150 +            request_handler: A WebSocketRequestHandler instance.
   1.151 +        """
   1.152 +
   1.153 +        self._request_handler = request_handler
   1.154 +
   1.155 +    def get_local_addr(self):
   1.156 +        """Getter to mimic mp_conn.local_addr."""
   1.157 +
   1.158 +        return (self._request_handler.server.server_name,
   1.159 +                self._request_handler.server.server_port)
   1.160 +    local_addr = property(get_local_addr)
   1.161 +
   1.162 +    def get_remote_addr(self):
   1.163 +        """Getter to mimic mp_conn.remote_addr.
   1.164 +
   1.165 +        Setting the property in __init__ won't work because the request
   1.166 +        handler is not initialized yet there."""
   1.167 +
   1.168 +        return self._request_handler.client_address
   1.169 +    remote_addr = property(get_remote_addr)
   1.170 +
   1.171 +    def write(self, data):
   1.172 +        """Mimic mp_conn.write()."""
   1.173 +
   1.174 +        return self._request_handler.wfile.write(data)
   1.175 +
   1.176 +    def read(self, length):
   1.177 +        """Mimic mp_conn.read()."""
   1.178 +
   1.179 +        return self._request_handler.rfile.read(length)
   1.180 +
   1.181 +    def get_memorized_lines(self):
   1.182 +        """Get memorized lines."""
   1.183 +
   1.184 +        return self._request_handler.rfile.get_memorized_lines()
   1.185 +
   1.186 +
   1.187 +class _StandaloneRequest(object):
   1.188 +    """Mimic mod_python request."""
   1.189 +
   1.190 +    def __init__(self, request_handler, use_tls):
   1.191 +        """Construct an instance.
   1.192 +
   1.193 +        Args:
   1.194 +            request_handler: A WebSocketRequestHandler instance.
   1.195 +        """
   1.196 +
   1.197 +        self._logger = util.get_class_logger(self)
   1.198 +
   1.199 +        self._request_handler = request_handler
   1.200 +        self.connection = _StandaloneConnection(request_handler)
   1.201 +        self._use_tls = use_tls
   1.202 +        self.headers_in = request_handler.headers
   1.203 +
   1.204 +    def get_uri(self):
   1.205 +        """Getter to mimic request.uri."""
   1.206 +
   1.207 +        return self._request_handler.path
   1.208 +    uri = property(get_uri)
   1.209 +
   1.210 +    def get_method(self):
   1.211 +        """Getter to mimic request.method."""
   1.212 +
   1.213 +        return self._request_handler.command
   1.214 +    method = property(get_method)
   1.215 +
   1.216 +    def is_https(self):
   1.217 +        """Mimic request.is_https()."""
   1.218 +
   1.219 +        return self._use_tls
   1.220 +
   1.221 +    def _drain_received_data(self):
   1.222 +        """Don't use this method from WebSocket handler. Drains unread data
   1.223 +        in the receive buffer.
   1.224 +        """
   1.225 +
   1.226 +        raw_socket = self._request_handler.connection
   1.227 +        drained_data = util.drain_received_data(raw_socket)
   1.228 +
   1.229 +        if drained_data:
   1.230 +            self._logger.debug(
   1.231 +                'Drained data following close frame: %r', drained_data)
   1.232 +
   1.233 +
   1.234 +class _StandaloneSSLConnection(object):
   1.235 +    """A wrapper class for OpenSSL.SSL.Connection to provide makefile method
   1.236 +    which is not supported by the class.
   1.237 +    """
   1.238 +
   1.239 +    def __init__(self, connection):
   1.240 +        self._connection = connection
   1.241 +
   1.242 +    def __getattribute__(self, name):
   1.243 +        if name in ('_connection', 'makefile'):
   1.244 +            return object.__getattribute__(self, name)
   1.245 +        return self._connection.__getattribute__(name)
   1.246 +
   1.247 +    def __setattr__(self, name, value):
   1.248 +        if name in ('_connection', 'makefile'):
   1.249 +            return object.__setattr__(self, name, value)
   1.250 +        return self._connection.__setattr__(name, value)
   1.251 +
   1.252 +    def makefile(self, mode='r', bufsize=-1):
   1.253 +        return socket._fileobject(self._connection, mode, bufsize)
   1.254 +
   1.255 +
   1.256 +class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
   1.257 +    """HTTPServer specialized for WebSocket."""
   1.258 +
   1.259 +    # Overrides SocketServer.ThreadingMixIn.daemon_threads
   1.260 +    daemon_threads = True
   1.261 +    # Overrides BaseHTTPServer.HTTPServer.allow_reuse_address
   1.262 +    allow_reuse_address = True
   1.263 +
   1.264 +    def __init__(self, options):
   1.265 +        """Override SocketServer.TCPServer.__init__ to set SSL enabled
   1.266 +        socket object to self.socket before server_bind and server_activate,
   1.267 +        if necessary.
   1.268 +        """
   1.269 +
   1.270 +        self._logger = util.get_class_logger(self)
   1.271 +
   1.272 +        self.request_queue_size = options.request_queue_size
   1.273 +        self.__ws_is_shut_down = threading.Event()
   1.274 +        self.__ws_serving = False
   1.275 +
   1.276 +        SocketServer.BaseServer.__init__(
   1.277 +            self, (options.server_host, options.port), WebSocketRequestHandler)
   1.278 +
   1.279 +        # Expose the options object to allow handler objects access it. We name
   1.280 +        # it with websocket_ prefix to avoid conflict.
   1.281 +        self.websocket_server_options = options
   1.282 +
   1.283 +        self._create_sockets()
   1.284 +        self.server_bind()
   1.285 +        self.server_activate()
   1.286 +
   1.287 +    def _create_sockets(self):
   1.288 +        self.server_name, self.server_port = self.server_address
   1.289 +        self._sockets = []
   1.290 +        if not self.server_name:
   1.291 +            # On platforms that doesn't support IPv6, the first bind fails.
   1.292 +            # On platforms that supports IPv6
   1.293 +            # - If it binds both IPv4 and IPv6 on call with AF_INET6, the
   1.294 +            #   first bind succeeds and the second fails (we'll see 'Address
   1.295 +            #   already in use' error).
   1.296 +            # - If it binds only IPv6 on call with AF_INET6, both call are
   1.297 +            #   expected to succeed to listen both protocol.
   1.298 +            addrinfo_array = [
   1.299 +                (socket.AF_INET6, socket.SOCK_STREAM, '', '', ''),
   1.300 +                (socket.AF_INET, socket.SOCK_STREAM, '', '', '')]
   1.301 +        else:
   1.302 +            addrinfo_array = socket.getaddrinfo(self.server_name,
   1.303 +                                                self.server_port,
   1.304 +                                                socket.AF_UNSPEC,
   1.305 +                                                socket.SOCK_STREAM,
   1.306 +                                                socket.IPPROTO_TCP)
   1.307 +        for addrinfo in addrinfo_array:
   1.308 +            self._logger.info('Create socket on: %r', addrinfo)
   1.309 +            family, socktype, proto, canonname, sockaddr = addrinfo
   1.310 +            try:
   1.311 +                socket_ = socket.socket(family, socktype)
   1.312 +            except Exception, e:
   1.313 +                self._logger.info('Skip by failure: %r', e)
   1.314 +                continue
   1.315 +            if self.websocket_server_options.use_tls:
   1.316 +                if _HAS_SSL:
   1.317 +                    socket_ = ssl.wrap_socket(socket_,
   1.318 +                        keyfile=self.websocket_server_options.private_key,
   1.319 +                        certfile=self.websocket_server_options.certificate,
   1.320 +                        ssl_version=ssl.PROTOCOL_SSLv23)
   1.321 +                if _HAS_OPEN_SSL:
   1.322 +                    ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
   1.323 +                    ctx.use_privatekey_file(
   1.324 +                        self.websocket_server_options.private_key)
   1.325 +                    ctx.use_certificate_file(
   1.326 +                        self.websocket_server_options.certificate)
   1.327 +                    socket_ = OpenSSL.SSL.Connection(ctx, socket_)
   1.328 +            self._sockets.append((socket_, addrinfo))
   1.329 +
   1.330 +    def server_bind(self):
   1.331 +        """Override SocketServer.TCPServer.server_bind to enable multiple
   1.332 +        sockets bind.
   1.333 +        """
   1.334 +
   1.335 +        failed_sockets = []
   1.336 +
   1.337 +        for socketinfo in self._sockets:
   1.338 +            socket_, addrinfo = socketinfo
   1.339 +            self._logger.info('Bind on: %r', addrinfo)
   1.340 +            if self.allow_reuse_address:
   1.341 +                socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
   1.342 +            try:
   1.343 +                socket_.bind(self.server_address)
   1.344 +            except Exception, e:
   1.345 +                self._logger.info('Skip by failure: %r', e)
   1.346 +                socket_.close()
   1.347 +                failed_sockets.append(socketinfo)
   1.348 +
   1.349 +        for socketinfo in failed_sockets:
   1.350 +            self._sockets.remove(socketinfo)
   1.351 +
   1.352 +    def server_activate(self):
   1.353 +        """Override SocketServer.TCPServer.server_activate to enable multiple
   1.354 +        sockets listen.
   1.355 +        """
   1.356 +
   1.357 +        failed_sockets = []
   1.358 +
   1.359 +        for socketinfo in self._sockets:
   1.360 +            socket_, addrinfo = socketinfo
   1.361 +            self._logger.info('Listen on: %r', addrinfo)
   1.362 +            try:
   1.363 +                socket_.listen(self.request_queue_size)
   1.364 +            except Exception, e:
   1.365 +                self._logger.info('Skip by failure: %r', e)
   1.366 +                socket_.close()
   1.367 +                failed_sockets.append(socketinfo)
   1.368 +
   1.369 +        for socketinfo in failed_sockets:
   1.370 +            self._sockets.remove(socketinfo)
   1.371 +
   1.372 +    def server_close(self):
   1.373 +        """Override SocketServer.TCPServer.server_close to enable multiple
   1.374 +        sockets close.
   1.375 +        """
   1.376 +
   1.377 +        for socketinfo in self._sockets:
   1.378 +            socket_, addrinfo = socketinfo
   1.379 +            self._logger.info('Close on: %r', addrinfo)
   1.380 +            socket_.close()
   1.381 +
   1.382 +    def fileno(self):
   1.383 +        """Override SocketServer.TCPServer.fileno."""
   1.384 +
   1.385 +        self._logger.critical('Not supported: fileno')
   1.386 +        return self._sockets[0][0].fileno()
   1.387 +
   1.388 +    def handle_error(self, rquest, client_address):
   1.389 +        """Override SocketServer.handle_error."""
   1.390 +
   1.391 +        self._logger.error(
   1.392 +            'Exception in processing request from: %r\n%s',
   1.393 +            client_address,
   1.394 +            util.get_stack_trace())
   1.395 +        # Note: client_address is a tuple.
   1.396 +
   1.397 +    def get_request(self):
   1.398 +        """Override TCPServer.get_request to wrap OpenSSL.SSL.Connection
   1.399 +        object with _StandaloneSSLConnection to provide makefile method. We
   1.400 +        cannot substitute OpenSSL.SSL.Connection.makefile since it's readonly
   1.401 +        attribute.
   1.402 +        """
   1.403 +
   1.404 +        accepted_socket, client_address = self.socket.accept()
   1.405 +        if self.websocket_server_options.use_tls and _HAS_OPEN_SSL:
   1.406 +            accepted_socket = _StandaloneSSLConnection(accepted_socket)
   1.407 +        return accepted_socket, client_address
   1.408 +
   1.409 +    def serve_forever(self, poll_interval=0.5):
   1.410 +        """Override SocketServer.BaseServer.serve_forever."""
   1.411 +
   1.412 +        self.__ws_serving = True
   1.413 +        self.__ws_is_shut_down.clear()
   1.414 +        handle_request = self.handle_request
   1.415 +        if hasattr(self, '_handle_request_noblock'):
   1.416 +            handle_request = self._handle_request_noblock
   1.417 +        else:
   1.418 +            self._logger.warning('Fallback to blocking request handler')
   1.419 +        try:
   1.420 +            while self.__ws_serving:
   1.421 +                r, w, e = select.select(
   1.422 +                    [socket_[0] for socket_ in self._sockets],
   1.423 +                    [], [], poll_interval)
   1.424 +                for socket_ in r:
   1.425 +                    self.socket = socket_
   1.426 +                    handle_request()
   1.427 +                self.socket = None
   1.428 +        finally:
   1.429 +            self.__ws_is_shut_down.set()
   1.430 +
   1.431 +    def shutdown(self):
   1.432 +        """Override SocketServer.BaseServer.shutdown."""
   1.433 +
   1.434 +        self.__ws_serving = False
   1.435 +        self.__ws_is_shut_down.wait()
   1.436 +
   1.437 +
   1.438 +class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
   1.439 +    """CGIHTTPRequestHandler specialized for WebSocket."""
   1.440 +
   1.441 +    # Use httplib.HTTPMessage instead of mimetools.Message.
   1.442 +    MessageClass = httplib.HTTPMessage
   1.443 +
   1.444 +    def setup(self):
   1.445 +        """Override SocketServer.StreamRequestHandler.setup to wrap rfile
   1.446 +        with MemorizingFile.
   1.447 +
   1.448 +        This method will be called by BaseRequestHandler's constructor
   1.449 +        before calling BaseHTTPRequestHandler.handle.
   1.450 +        BaseHTTPRequestHandler.handle will call
   1.451 +        BaseHTTPRequestHandler.handle_one_request and it will call
   1.452 +        WebSocketRequestHandler.parse_request.
   1.453 +        """
   1.454 +
   1.455 +        # Call superclass's setup to prepare rfile, wfile, etc. See setup
   1.456 +        # definition on the root class SocketServer.StreamRequestHandler to
   1.457 +        # understand what this does.
   1.458 +        CGIHTTPServer.CGIHTTPRequestHandler.setup(self)
   1.459 +
   1.460 +        self.rfile = memorizingfile.MemorizingFile(
   1.461 +            self.rfile,
   1.462 +            max_memorized_lines=_MAX_MEMORIZED_LINES)
   1.463 +
   1.464 +    def __init__(self, request, client_address, server):
   1.465 +        self._logger = util.get_class_logger(self)
   1.466 +
   1.467 +        self._options = server.websocket_server_options
   1.468 +
   1.469 +        # Overrides CGIHTTPServerRequestHandler.cgi_directories.
   1.470 +        self.cgi_directories = self._options.cgi_directories
   1.471 +        # Replace CGIHTTPRequestHandler.is_executable method.
   1.472 +        if self._options.is_executable_method is not None:
   1.473 +            self.is_executable = self._options.is_executable_method
   1.474 +
   1.475 +        # This actually calls BaseRequestHandler.__init__.
   1.476 +        CGIHTTPServer.CGIHTTPRequestHandler.__init__(
   1.477 +            self, request, client_address, server)
   1.478 +
   1.479 +    def parse_request(self):
   1.480 +        """Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request.
   1.481 +
   1.482 +        Return True to continue processing for HTTP(S), False otherwise.
   1.483 +
   1.484 +        See BaseHTTPRequestHandler.handle_one_request method which calls
   1.485 +        this method to understand how the return value will be handled.
   1.486 +        """
   1.487 +
   1.488 +        # We hook parse_request method, but also call the original
   1.489 +        # CGIHTTPRequestHandler.parse_request since when we return False,
   1.490 +        # CGIHTTPRequestHandler.handle_one_request continues processing and
   1.491 +        # it needs variables set by CGIHTTPRequestHandler.parse_request.
   1.492 +        #
   1.493 +        # Variables set by this method will be also used by WebSocket request
   1.494 +        # handling (self.path, self.command, self.requestline, etc. See also
   1.495 +        # how _StandaloneRequest's members are implemented using these
   1.496 +        # attributes).
   1.497 +        if not CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self):
   1.498 +            return False
   1.499 +        host, port, resource = http_header_util.parse_uri(self.path)
   1.500 +        if resource is None:
   1.501 +            self._logger.info('Invalid URI: %r', self.path)
   1.502 +            self._logger.info('Fallback to CGIHTTPRequestHandler')
   1.503 +            return True
   1.504 +        server_options = self.server.websocket_server_options
   1.505 +        if host is not None:
   1.506 +            validation_host = server_options.validation_host
   1.507 +            if validation_host is not None and host != validation_host:
   1.508 +                self._logger.info('Invalid host: %r (expected: %r)',
   1.509 +                                  host,
   1.510 +                                  validation_host)
   1.511 +                self._logger.info('Fallback to CGIHTTPRequestHandler')
   1.512 +                return True
   1.513 +        if port is not None:
   1.514 +            validation_port = server_options.validation_port
   1.515 +            if validation_port is not None and port != validation_port:
   1.516 +                self._logger.info('Invalid port: %r (expected: %r)',
   1.517 +                                  port,
   1.518 +                                  validation_port)
   1.519 +                self._logger.info('Fallback to CGIHTTPRequestHandler')
   1.520 +                return True
   1.521 +        self.path = resource
   1.522 +
   1.523 +        request = _StandaloneRequest(self, self._options.use_tls)
   1.524 +
   1.525 +        try:
   1.526 +            # Fallback to default http handler for request paths for which
   1.527 +            # we don't have request handlers.
   1.528 +            if not self._options.dispatcher.get_handler_suite(self.path):
   1.529 +                self._logger.info('No handler for resource: %r',
   1.530 +                                  self.path)
   1.531 +                self._logger.info('Fallback to CGIHTTPRequestHandler')
   1.532 +                return True
   1.533 +        except dispatch.DispatchException, e:
   1.534 +            self._logger.info('%s', e)
   1.535 +            self.send_error(e.status)
   1.536 +            return False
   1.537 +
   1.538 +        # If any Exceptions without except clause setup (including
   1.539 +        # DispatchException) is raised below this point, it will be caught
   1.540 +        # and logged by WebSocketServer.
   1.541 +
   1.542 +        try:
   1.543 +            try:
   1.544 +                handshake.do_handshake(
   1.545 +                    request,
   1.546 +                    self._options.dispatcher,
   1.547 +                    allowDraft75=self._options.allow_draft75,
   1.548 +                    strict=self._options.strict)
   1.549 +            except handshake.VersionException, e:
   1.550 +                self._logger.info('%s', e)
   1.551 +                self.send_response(common.HTTP_STATUS_BAD_REQUEST)
   1.552 +                self.send_header(common.SEC_WEBSOCKET_VERSION_HEADER,
   1.553 +                                 e.supported_versions)
   1.554 +                self.end_headers()
   1.555 +                return False
   1.556 +            except handshake.HandshakeException, e:
   1.557 +                # Handshake for ws(s) failed.
   1.558 +                self._logger.info('%s', e)
   1.559 +                self.send_error(e.status)
   1.560 +                return False
   1.561 +
   1.562 +            request._dispatcher = self._options.dispatcher
   1.563 +            self._options.dispatcher.transfer_data(request)
   1.564 +        except handshake.AbortedByUserException, e:
   1.565 +            self._logger.info('%s', e)
   1.566 +        return False
   1.567 +
   1.568 +    def log_request(self, code='-', size='-'):
   1.569 +        """Override BaseHTTPServer.log_request."""
   1.570 +
   1.571 +        self._logger.info('"%s" %s %s',
   1.572 +                          self.requestline, str(code), str(size))
   1.573 +
   1.574 +    def log_error(self, *args):
   1.575 +        """Override BaseHTTPServer.log_error."""
   1.576 +
   1.577 +        # Despite the name, this method is for warnings than for errors.
   1.578 +        # For example, HTTP status code is logged by this method.
   1.579 +        self._logger.warning('%s - %s',
   1.580 +                             self.address_string(),
   1.581 +                             args[0] % args[1:])
   1.582 +
   1.583 +    def is_cgi(self):
   1.584 +        """Test whether self.path corresponds to a CGI script.
   1.585 +
   1.586 +        Add extra check that self.path doesn't contains ..
   1.587 +        Also check if the file is a executable file or not.
   1.588 +        If the file is not executable, it is handled as static file or dir
   1.589 +        rather than a CGI script.
   1.590 +        """
   1.591 +
   1.592 +        if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self):
   1.593 +            if '..' in self.path:
   1.594 +                return False
   1.595 +            # strip query parameter from request path
   1.596 +            resource_name = self.path.split('?', 2)[0]
   1.597 +            # convert resource_name into real path name in filesystem.
   1.598 +            scriptfile = self.translate_path(resource_name)
   1.599 +            if not os.path.isfile(scriptfile):
   1.600 +                return False
   1.601 +            if not self.is_executable(scriptfile):
   1.602 +                return False
   1.603 +            return True
   1.604 +        return False
   1.605 +
   1.606 +
   1.607 +def _configure_logging(options):
   1.608 +    logger = logging.getLogger()
   1.609 +    logger.setLevel(logging.getLevelName(options.log_level.upper()))
   1.610 +    if options.log_file:
   1.611 +        handler = logging.handlers.RotatingFileHandler(
   1.612 +                options.log_file, 'a', options.log_max, options.log_count)
   1.613 +    else:
   1.614 +        handler = logging.StreamHandler()
   1.615 +    formatter = logging.Formatter(
   1.616 +            '[%(asctime)s] [%(levelname)s] %(name)s: %(message)s')
   1.617 +    handler.setFormatter(formatter)
   1.618 +    logger.addHandler(handler)
   1.619 +
   1.620 +
   1.621 +def _alias_handlers(dispatcher, websock_handlers_map_file):
   1.622 +    """Set aliases specified in websock_handler_map_file in dispatcher.
   1.623 +
   1.624 +    Args:
   1.625 +        dispatcher: dispatch.Dispatcher instance
   1.626 +        websock_handler_map_file: alias map file
   1.627 +    """
   1.628 +
   1.629 +    fp = open(websock_handlers_map_file)
   1.630 +    try:
   1.631 +        for line in fp:
   1.632 +            if line[0] == '#' or line.isspace():
   1.633 +                continue
   1.634 +            m = re.match('(\S+)\s+(\S+)', line)
   1.635 +            if not m:
   1.636 +                logging.warning('Wrong format in map file:' + line)
   1.637 +                continue
   1.638 +            try:
   1.639 +                dispatcher.add_resource_path_alias(
   1.640 +                    m.group(1), m.group(2))
   1.641 +            except dispatch.DispatchException, e:
   1.642 +                logging.error(str(e))
   1.643 +    finally:
   1.644 +        fp.close()
   1.645 +
   1.646 +
   1.647 +def _build_option_parser():
   1.648 +    parser = optparse.OptionParser()
   1.649 +
   1.650 +    parser.add_option('--config', dest='config_file', type='string',
   1.651 +                      default=None,
   1.652 +                      help=('Path to configuration file. See the file comment '
   1.653 +                            'at the top of this file for the configuration '
   1.654 +                            'file format'))
   1.655 +    parser.add_option('-H', '--server-host', '--server_host',
   1.656 +                      dest='server_host',
   1.657 +                      default='',
   1.658 +                      help='server hostname to listen to')
   1.659 +    parser.add_option('-V', '--validation-host', '--validation_host',
   1.660 +                      dest='validation_host',
   1.661 +                      default=None,
   1.662 +                      help='server hostname to validate in absolute path.')
   1.663 +    parser.add_option('-p', '--port', dest='port', type='int',
   1.664 +                      default=common.DEFAULT_WEB_SOCKET_PORT,
   1.665 +                      help='port to listen to')
   1.666 +    parser.add_option('-P', '--validation-port', '--validation_port',
   1.667 +                      dest='validation_port', type='int',
   1.668 +                      default=None,
   1.669 +                      help='server port to validate in absolute path.')
   1.670 +    parser.add_option('-w', '--websock-handlers', '--websock_handlers',
   1.671 +                      dest='websock_handlers',
   1.672 +                      default='.',
   1.673 +                      help='WebSocket handlers root directory.')
   1.674 +    parser.add_option('-m', '--websock-handlers-map-file',
   1.675 +                      '--websock_handlers_map_file',
   1.676 +                      dest='websock_handlers_map_file',
   1.677 +                      default=None,
   1.678 +                      help=('WebSocket handlers map file. '
   1.679 +                            'Each line consists of alias_resource_path and '
   1.680 +                            'existing_resource_path, separated by spaces.'))
   1.681 +    parser.add_option('-s', '--scan-dir', '--scan_dir', dest='scan_dir',
   1.682 +                      default=None,
   1.683 +                      help=('WebSocket handlers scan directory. '
   1.684 +                            'Must be a directory under websock_handlers.'))
   1.685 +    parser.add_option('--allow-handlers-outside-root-dir',
   1.686 +                      '--allow_handlers_outside_root_dir',
   1.687 +                      dest='allow_handlers_outside_root_dir',
   1.688 +                      action='store_true',
   1.689 +                      default=False,
   1.690 +                      help=('Scans WebSocket handlers even if their canonical '
   1.691 +                            'path is not under websock_handlers.'))
   1.692 +    parser.add_option('-d', '--document-root', '--document_root',
   1.693 +                      dest='document_root', default='.',
   1.694 +                      help='Document root directory.')
   1.695 +    parser.add_option('-x', '--cgi-paths', '--cgi_paths', dest='cgi_paths',
   1.696 +                      default=None,
   1.697 +                      help=('CGI paths relative to document_root.'
   1.698 +                            'Comma-separated. (e.g -x /cgi,/htbin) '
   1.699 +                            'Files under document_root/cgi_path are handled '
   1.700 +                            'as CGI programs. Must be executable.'))
   1.701 +    parser.add_option('-t', '--tls', dest='use_tls', action='store_true',
   1.702 +                      default=False, help='use TLS (wss://)')
   1.703 +    parser.add_option('-k', '--private-key', '--private_key',
   1.704 +                      dest='private_key',
   1.705 +                      default='', help='TLS private key file.')
   1.706 +    parser.add_option('-c', '--certificate', dest='certificate',
   1.707 +                      default='', help='TLS certificate file.')
   1.708 +    parser.add_option('-l', '--log-file', '--log_file', dest='log_file',
   1.709 +                      default='', help='Log file.')
   1.710 +    parser.add_option('--log-level', '--log_level', type='choice',
   1.711 +                      dest='log_level', default='warn',
   1.712 +                      choices=['debug', 'info', 'warning', 'warn', 'error',
   1.713 +                               'critical'],
   1.714 +                      help='Log level.')
   1.715 +    parser.add_option('--thread-monitor-interval-in-sec',
   1.716 +                      '--thread_monitor_interval_in_sec',
   1.717 +                      dest='thread_monitor_interval_in_sec',
   1.718 +                      type='int', default=-1,
   1.719 +                      help=('If positive integer is specified, run a thread '
   1.720 +                            'monitor to show the status of server threads '
   1.721 +                            'periodically in the specified inteval in '
   1.722 +                            'second. If non-positive integer is specified, '
   1.723 +                            'disable the thread monitor.'))
   1.724 +    parser.add_option('--log-max', '--log_max', dest='log_max', type='int',
   1.725 +                      default=_DEFAULT_LOG_MAX_BYTES,
   1.726 +                      help='Log maximum bytes')
   1.727 +    parser.add_option('--log-count', '--log_count', dest='log_count',
   1.728 +                      type='int', default=_DEFAULT_LOG_BACKUP_COUNT,
   1.729 +                      help='Log backup count')
   1.730 +    parser.add_option('--allow-draft75', dest='allow_draft75',
   1.731 +                      action='store_true', default=False,
   1.732 +                      help='Allow draft 75 handshake')
   1.733 +    parser.add_option('--strict', dest='strict', action='store_true',
   1.734 +                      default=False, help='Strictly check handshake request')
   1.735 +    parser.add_option('-q', '--queue', dest='request_queue_size', type='int',
   1.736 +                      default=_DEFAULT_REQUEST_QUEUE_SIZE,
   1.737 +                      help='request queue size')
   1.738 +
   1.739 +    return parser
   1.740 +
   1.741 +
   1.742 +class ThreadMonitor(threading.Thread):
   1.743 +    daemon = True
   1.744 +
   1.745 +    def __init__(self, interval_in_sec):
   1.746 +        threading.Thread.__init__(self, name='ThreadMonitor')
   1.747 +
   1.748 +        self._logger = util.get_class_logger(self)
   1.749 +
   1.750 +        self._interval_in_sec = interval_in_sec
   1.751 +
   1.752 +    def run(self):
   1.753 +        while True:
   1.754 +            thread_name_list = []
   1.755 +            for thread in threading.enumerate():
   1.756 +                thread_name_list.append(thread.name)
   1.757 +            self._logger.info(
   1.758 +                "%d active threads: %s",
   1.759 +                threading.active_count(),
   1.760 +                ', '.join(thread_name_list))
   1.761 +            time.sleep(self._interval_in_sec)
   1.762 +
   1.763 +
   1.764 +def _parse_args_and_config(args):
   1.765 +    parser = _build_option_parser()
   1.766 +
   1.767 +    # First, parse options without configuration file.
   1.768 +    temporary_options, temporary_args = parser.parse_args(args=args)
   1.769 +    if temporary_args:
   1.770 +        logging.critical(
   1.771 +            'Unrecognized positional arguments: %r', temporary_args)
   1.772 +        sys.exit(1)
   1.773 +
   1.774 +    if temporary_options.config_file:
   1.775 +        try:
   1.776 +            config_fp = open(temporary_options.config_file, 'r')
   1.777 +        except IOError, e:
   1.778 +            logging.critical(
   1.779 +                'Failed to open configuration file %r: %r',
   1.780 +                temporary_options.config_file,
   1.781 +                e)
   1.782 +            sys.exit(1)
   1.783 +
   1.784 +        config_parser = ConfigParser.SafeConfigParser()
   1.785 +        config_parser.readfp(config_fp)
   1.786 +        config_fp.close()
   1.787 +
   1.788 +        args_from_config = []
   1.789 +        for name, value in config_parser.items('pywebsocket'):
   1.790 +            args_from_config.append('--' + name)
   1.791 +            args_from_config.append(value)
   1.792 +        if args is None:
   1.793 +            args = args_from_config
   1.794 +        else:
   1.795 +            args = args_from_config + args
   1.796 +        return parser.parse_args(args=args)
   1.797 +    else:
   1.798 +        return temporary_options, temporary_args
   1.799 +
   1.800 +
   1.801 +def _main(args=None):
   1.802 +    options, args = _parse_args_and_config(args=args)
   1.803 +
   1.804 +    os.chdir(options.document_root)
   1.805 +
   1.806 +    _configure_logging(options)
   1.807 +
   1.808 +    # TODO(tyoshino): Clean up initialization of CGI related values. Move some
   1.809 +    # of code here to WebSocketRequestHandler class if it's better.
   1.810 +    options.cgi_directories = []
   1.811 +    options.is_executable_method = None
   1.812 +    if options.cgi_paths:
   1.813 +        options.cgi_directories = options.cgi_paths.split(',')
   1.814 +        if sys.platform in ('cygwin', 'win32'):
   1.815 +            cygwin_path = None
   1.816 +            # For Win32 Python, it is expected that CYGWIN_PATH
   1.817 +            # is set to a directory of cygwin binaries.
   1.818 +            # For example, websocket_server.py in Chromium sets CYGWIN_PATH to
   1.819 +            # full path of third_party/cygwin/bin.
   1.820 +            if 'CYGWIN_PATH' in os.environ:
   1.821 +                cygwin_path = os.environ['CYGWIN_PATH']
   1.822 +            util.wrap_popen3_for_win(cygwin_path)
   1.823 +
   1.824 +            def __check_script(scriptpath):
   1.825 +                return util.get_script_interp(scriptpath, cygwin_path)
   1.826 +
   1.827 +            options.is_executable_method = __check_script
   1.828 +
   1.829 +    if options.use_tls:
   1.830 +        if not (_HAS_SSL or _HAS_OPEN_SSL):
   1.831 +            logging.critical('TLS support requires ssl or pyOpenSSL.')
   1.832 +            sys.exit(1)
   1.833 +        if not options.private_key or not options.certificate:
   1.834 +            logging.critical(
   1.835 +                    'To use TLS, specify private_key and certificate.')
   1.836 +            sys.exit(1)
   1.837 +
   1.838 +    if not options.scan_dir:
   1.839 +        options.scan_dir = options.websock_handlers
   1.840 +
   1.841 +    try:
   1.842 +        if options.thread_monitor_interval_in_sec > 0:
   1.843 +            # Run a thread monitor to show the status of server threads for
   1.844 +            # debugging.
   1.845 +            ThreadMonitor(options.thread_monitor_interval_in_sec).start()
   1.846 +
   1.847 +        # Share a Dispatcher among request handlers to save time for
   1.848 +        # instantiation.  Dispatcher can be shared because it is thread-safe.
   1.849 +        options.dispatcher = dispatch.Dispatcher(
   1.850 +            options.websock_handlers,
   1.851 +            options.scan_dir,
   1.852 +            options.allow_handlers_outside_root_dir)
   1.853 +        if options.websock_handlers_map_file:
   1.854 +            _alias_handlers(options.dispatcher,
   1.855 +                            options.websock_handlers_map_file)
   1.856 +        warnings = options.dispatcher.source_warnings()
   1.857 +        if warnings:
   1.858 +            for warning in warnings:
   1.859 +                logging.warning('mod_pywebsocket: %s' % warning)
   1.860 +
   1.861 +        server = WebSocketServer(options)
   1.862 +        server.serve_forever()
   1.863 +    except Exception, e:
   1.864 +        logging.critical('mod_pywebsocket: %s' % e)
   1.865 +        logging.critical('mod_pywebsocket: %s' % util.get_stack_trace())
   1.866 +        sys.exit(1)
   1.867 +
   1.868 +
   1.869 +if __name__ == '__main__':
   1.870 +    _main(sys.argv[1:])
   1.871 +
   1.872 +
   1.873 +# vi:sts=4 sw=4 et

mercurial