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