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