Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
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 |