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