testing/mochitest/pywebsocket/standalone.py

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rwxr-xr-x

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial