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