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