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
35Use this file to launch pywebsocket without Apache HTTP Server.
36
37
38BASIC USAGE
39===========
40
41Go to the src directory and run
42
43  $ python mod_pywebsocket/standalone.py [-p <ws_port>]
44                                         [-w <websock_handlers>]
45                                         [-d <document_root>]
46
47<ws_port> is the port number to use for ws:// connection.
48
49<document_root> is the path to the root directory of HTML files.
50
51<websock_handlers> is the path to the root directory of WebSocket handlers.
52If not specified, <document_root> will be used. See __init__.py (or
53run $ pydoc mod_pywebsocket) for how to write WebSocket handlers.
54
55For more detail and other options, run
56
57  $ python mod_pywebsocket/standalone.py --help
58
59or see _build_option_parser method below.
60
61For trouble shooting, adding "--log_level debug" might help you.
62
63
64TRY DEMO
65========
66
67Go to the src directory and run standalone.py with -d option to set the
68document root to the directory containing example HTMLs and handlers like this:
69
70  $ cd src
71  $ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example
72
73to launch pywebsocket with the sample handler and html on port 80. Open
74http://localhost/console.html, click the connect button, type something into
75the text box next to the send button and click the send button. If everything
76is working, you'll see the message you typed echoed by the server.
77
78
79USING TLS
80=========
81
82To run the standalone server with TLS support, run it with -t, -k, and -c
83options. When TLS is enabled, the standalone server accepts only TLS connection.
84
85Note that when ssl module is used and the key/cert location is incorrect,
86TLS connection silently fails while pyOpenSSL fails on startup.
87
88Example:
89
90  $ PYTHONPATH=. python mod_pywebsocket/standalone.py \
91        -d example \
92        -p 10443 \
93        -t \
94        -c ../test/cert/cert.pem \
95        -k ../test/cert/key.pem \
96
97Note that when passing a relative path to -c and -k option, it will be resolved
98using the document root directory as the base.
99
100
101USING CLIENT AUTHENTICATION
102===========================
103
104To run the standalone server with TLS client authentication support, run it with
105--tls-client-auth and --tls-client-ca options in addition to ones required for
106TLS support.
107
108Example:
109
110  $ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example -p 10443 -t \
111        -c ../test/cert/cert.pem -k ../test/cert/key.pem \
112        --tls-client-auth \
113        --tls-client-ca=../test/cert/cacert.pem
114
115Note that when passing a relative path to --tls-client-ca option, it will be
116resolved using the document root directory as the base.
117
118
119CONFIGURATION FILE
120==================
121
122You can also write a configuration file and use it by specifying the path to
123the configuration file by --config option. Please write a configuration file
124following the documentation of the Python ConfigParser library. Name of each
125entry must be the long version argument name. E.g. to set log level to debug,
126add the following line:
127
128log_level=debug
129
130For options which doesn't take value, please add some fake value. E.g. for
131--tls option, add the following line:
132
133tls=True
134
135Note that tls will be enabled even if you write tls=False as the value part is
136fake.
137
138When both a command line argument and a configuration file entry are set for
139the same configuration item, the command line value will override one in the
140configuration file.
141
142
143THREADING
144=========
145
146This server is derived from SocketServer.ThreadingMixIn. Hence a thread is
147used for each request.
148
149
150SECURITY WARNING
151================
152
153This uses CGIHTTPServer and CGIHTTPServer is not secure.
154It may execute arbitrary Python code or external programs. It should not be
155used outside a firewall.
156"""
157
158import BaseHTTPServer
159import CGIHTTPServer
160import SimpleHTTPServer
161import SocketServer
162import ConfigParser
163import base64
164import httplib
165import logging
166import logging.handlers
167import optparse
168import os
169import re
170import select
171import socket
172import sys
173import threading
174import time
175
176from mod_pywebsocket import common
177from mod_pywebsocket import dispatch
178from mod_pywebsocket import handshake
179from mod_pywebsocket import http_header_util
180from mod_pywebsocket import memorizingfile
181from mod_pywebsocket import util
182from mod_pywebsocket.xhr_benchmark_handler import XHRBenchmarkHandler
183
184
185_DEFAULT_LOG_MAX_BYTES = 1024 * 256
186_DEFAULT_LOG_BACKUP_COUNT = 5
187
188_DEFAULT_REQUEST_QUEUE_SIZE = 128
189
190# 1024 is practically large enough to contain WebSocket handshake lines.
191_MAX_MEMORIZED_LINES = 1024
192
193# Constants for the --tls_module flag.
194_TLS_BY_STANDARD_MODULE = 'ssl'
195_TLS_BY_PYOPENSSL = 'pyopenssl'
196
197
198class _StandaloneConnection(object):
199    """Mimic mod_python mp_conn."""
200
201    def __init__(self, request_handler):
202        """Construct an instance.
203
204        Args:
205            request_handler: A WebSocketRequestHandler instance.
206        """
207
208        self._request_handler = request_handler
209
210    def get_local_addr(self):
211        """Getter to mimic mp_conn.local_addr."""
212
213        return (self._request_handler.server.server_name,
214                self._request_handler.server.server_port)
215    local_addr = property(get_local_addr)
216
217    def get_remote_addr(self):
218        """Getter to mimic mp_conn.remote_addr.
219
220        Setting the property in __init__ won't work because the request
221        handler is not initialized yet there."""
222
223        return self._request_handler.client_address
224    remote_addr = property(get_remote_addr)
225
226    def write(self, data):
227        """Mimic mp_conn.write()."""
228
229        return self._request_handler.wfile.write(data)
230
231    def read(self, length):
232        """Mimic mp_conn.read()."""
233
234        return self._request_handler.rfile.read(length)
235
236    def get_memorized_lines(self):
237        """Get memorized lines."""
238
239        return self._request_handler.rfile.get_memorized_lines()
240
241
242class _StandaloneRequest(object):
243    """Mimic mod_python request."""
244
245    def __init__(self, request_handler, use_tls):
246        """Construct an instance.
247
248        Args:
249            request_handler: A WebSocketRequestHandler instance.
250        """
251
252        self._logger = util.get_class_logger(self)
253
254        self._request_handler = request_handler
255        self.connection = _StandaloneConnection(request_handler)
256        self._use_tls = use_tls
257        self.headers_in = request_handler.headers
258
259    def get_uri(self):
260        """Getter to mimic request.uri.
261
262        This method returns the raw data at the Request-URI part of the
263        Request-Line, while the uri method on the request object of mod_python
264        returns the path portion after parsing the raw data. This behavior is
265        kept for compatibility.
266        """
267
268        return self._request_handler.path
269    uri = property(get_uri)
270
271    def get_unparsed_uri(self):
272        """Getter to mimic request.unparsed_uri."""
273
274        return self._request_handler.path
275    unparsed_uri = property(get_unparsed_uri)
276
277    def get_method(self):
278        """Getter to mimic request.method."""
279
280        return self._request_handler.command
281    method = property(get_method)
282
283    def get_protocol(self):
284        """Getter to mimic request.protocol."""
285
286        return self._request_handler.request_version
287    protocol = property(get_protocol)
288
289    def is_https(self):
290        """Mimic request.is_https()."""
291
292        return self._use_tls
293
294
295def _import_ssl():
296    global ssl
297    try:
298        import ssl
299        return True
300    except ImportError:
301        return False
302
303
304def _import_pyopenssl():
305    global OpenSSL
306    try:
307        import OpenSSL.SSL
308        return True
309    except ImportError:
310        return False
311
312
313class _StandaloneSSLConnection(object):
314    """A wrapper class for OpenSSL.SSL.Connection to
315    - provide makefile method which is not supported by the class
316    - tweak shutdown method since OpenSSL.SSL.Connection.shutdown doesn't
317      accept the "how" argument.
318    - convert SysCallError exceptions that its recv method may raise into a
319      return value of '', meaning EOF. We cannot overwrite the recv method on
320      self._connection since it's immutable.
321    """
322
323    _OVERRIDDEN_ATTRIBUTES = ['_connection', 'makefile', 'shutdown', 'recv']
324
325    def __init__(self, connection):
326        self._connection = connection
327
328    def __getattribute__(self, name):
329        if name in _StandaloneSSLConnection._OVERRIDDEN_ATTRIBUTES:
330            return object.__getattribute__(self, name)
331        return self._connection.__getattribute__(name)
332
333    def __setattr__(self, name, value):
334        if name in _StandaloneSSLConnection._OVERRIDDEN_ATTRIBUTES:
335            return object.__setattr__(self, name, value)
336        return self._connection.__setattr__(name, value)
337
338    def makefile(self, mode='r', bufsize=-1):
339        return socket._fileobject(self, mode, bufsize)
340
341    def shutdown(self, unused_how):
342        self._connection.shutdown()
343
344    def recv(self, bufsize, flags=0):
345        if flags != 0:
346            raise ValueError('Non-zero flags not allowed')
347
348        try:
349            return self._connection.recv(bufsize)
350        except OpenSSL.SSL.SysCallError, (err, message):
351            if err == -1:
352                # Suppress "unexpected EOF" exception. See the OpenSSL document
353                # for SSL_get_error.
354                return ''
355            raise
356
357
358def _alias_handlers(dispatcher, websock_handlers_map_file):
359    """Set aliases specified in websock_handler_map_file in dispatcher.
360
361    Args:
362        dispatcher: dispatch.Dispatcher instance
363        websock_handler_map_file: alias map file
364    """
365
366    fp = open(websock_handlers_map_file)
367    try:
368        for line in fp:
369            if line[0] == '#' or line.isspace():
370                continue
371            m = re.match('(\S+)\s+(\S+)', line)
372            if not m:
373                logging.warning('Wrong format in map file:' + line)
374                continue
375            try:
376                dispatcher.add_resource_path_alias(
377                    m.group(1), m.group(2))
378            except dispatch.DispatchException, e:
379                logging.error(str(e))
380    finally:
381        fp.close()
382
383
384class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
385    """HTTPServer specialized for WebSocket."""
386
387    # Overrides SocketServer.ThreadingMixIn.daemon_threads
388    daemon_threads = True
389    # Overrides BaseHTTPServer.HTTPServer.allow_reuse_address
390    allow_reuse_address = True
391
392    def __init__(self, options):
393        """Override SocketServer.TCPServer.__init__ to set SSL enabled
394        socket object to self.socket before server_bind and server_activate,
395        if necessary.
396        """
397
398        # Share a Dispatcher among request handlers to save time for
399        # instantiation.  Dispatcher can be shared because it is thread-safe.
400        options.dispatcher = dispatch.Dispatcher(
401            options.websock_handlers,
402            options.scan_dir,
403            options.allow_handlers_outside_root_dir)
404        if options.websock_handlers_map_file:
405            _alias_handlers(options.dispatcher,
406                            options.websock_handlers_map_file)
407        warnings = options.dispatcher.source_warnings()
408        if warnings:
409            for warning in warnings:
410                logging.warning('Warning in source loading: %s' % warning)
411
412        self._logger = util.get_class_logger(self)
413
414        self.request_queue_size = options.request_queue_size
415        self.__ws_is_shut_down = threading.Event()
416        self.__ws_serving = False
417
418        SocketServer.BaseServer.__init__(
419            self, (options.server_host, options.port), WebSocketRequestHandler)
420
421        # Expose the options object to allow handler objects access it. We name
422        # it with websocket_ prefix to avoid conflict.
423        self.websocket_server_options = options
424
425        self._create_sockets()
426        self.server_bind()
427        self.server_activate()
428
429    def _create_sockets(self):
430        self.server_name, self.server_port = self.server_address
431        self._sockets = []
432        if not self.server_name:
433            # On platforms that doesn't support IPv6, the first bind fails.
434            # On platforms that supports IPv6
435            # - If it binds both IPv4 and IPv6 on call with AF_INET6, the
436            #   first bind succeeds and the second fails (we'll see 'Address
437            #   already in use' error).
438            # - If it binds only IPv6 on call with AF_INET6, both call are
439            #   expected to succeed to listen both protocol.
440            addrinfo_array = [
441                (socket.AF_INET6, socket.SOCK_STREAM, '', '', ''),
442                (socket.AF_INET, socket.SOCK_STREAM, '', '', '')]
443        else:
444            addrinfo_array = socket.getaddrinfo(self.server_name,
445                                                self.server_port,
446                                                socket.AF_UNSPEC,
447                                                socket.SOCK_STREAM,
448                                                socket.IPPROTO_TCP)
449        for addrinfo in addrinfo_array:
450            self._logger.info('Create socket on: %r', addrinfo)
451            family, socktype, proto, canonname, sockaddr = addrinfo
452            try:
453                socket_ = socket.socket(family, socktype)
454            except Exception, e:
455                self._logger.info('Skip by failure: %r', e)
456                continue
457            server_options = self.websocket_server_options
458            if server_options.use_tls:
459                # For the case of _HAS_OPEN_SSL, we do wrapper setup after
460                # accept.
461                if server_options.tls_module == _TLS_BY_STANDARD_MODULE:
462                    if server_options.tls_client_auth:
463                        if server_options.tls_client_cert_optional:
464                            client_cert_ = ssl.CERT_OPTIONAL
465                        else:
466                            client_cert_ = ssl.CERT_REQUIRED
467                    else:
468                        client_cert_ = ssl.CERT_NONE
469                    socket_ = ssl.wrap_socket(socket_,
470                        keyfile=server_options.private_key,
471                        certfile=server_options.certificate,
472                        ssl_version=ssl.PROTOCOL_SSLv23,
473                        ca_certs=server_options.tls_client_ca,
474                        cert_reqs=client_cert_,
475                        do_handshake_on_connect=False)
476            self._sockets.append((socket_, addrinfo))
477
478    def server_bind(self):
479        """Override SocketServer.TCPServer.server_bind to enable multiple
480        sockets bind.
481        """
482
483        failed_sockets = []
484
485        for socketinfo in self._sockets:
486            socket_, addrinfo = socketinfo
487            self._logger.info('Bind on: %r', addrinfo)
488            if self.allow_reuse_address:
489                socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
490            try:
491                socket_.bind(self.server_address)
492            except Exception, e:
493                self._logger.info('Skip by failure: %r', e)
494                socket_.close()
495                failed_sockets.append(socketinfo)
496            if self.server_address[1] == 0:
497                # The operating system assigns the actual port number for port
498                # number 0. This case, the second and later sockets should use
499                # the same port number. Also self.server_port is rewritten
500                # because it is exported, and will be used by external code.
501                self.server_address = (
502                    self.server_name, socket_.getsockname()[1])
503                self.server_port = self.server_address[1]
504                self._logger.info('Port %r is assigned', self.server_port)
505
506        for socketinfo in failed_sockets:
507            self._sockets.remove(socketinfo)
508
509    def server_activate(self):
510        """Override SocketServer.TCPServer.server_activate to enable multiple
511        sockets listen.
512        """
513
514        failed_sockets = []
515
516        for socketinfo in self._sockets:
517            socket_, addrinfo = socketinfo
518            self._logger.info('Listen on: %r', addrinfo)
519            try:
520                socket_.listen(self.request_queue_size)
521            except Exception, e:
522                self._logger.info('Skip by failure: %r', e)
523                socket_.close()
524                failed_sockets.append(socketinfo)
525
526        for socketinfo in failed_sockets:
527            self._sockets.remove(socketinfo)
528
529        if len(self._sockets) == 0:
530            self._logger.critical(
531                'No sockets activated. Use info log level to see the reason.')
532
533    def server_close(self):
534        """Override SocketServer.TCPServer.server_close to enable multiple
535        sockets close.
536        """
537
538        for socketinfo in self._sockets:
539            socket_, addrinfo = socketinfo
540            self._logger.info('Close on: %r', addrinfo)
541            socket_.close()
542
543    def fileno(self):
544        """Override SocketServer.TCPServer.fileno."""
545
546        self._logger.critical('Not supported: fileno')
547        return self._sockets[0][0].fileno()
548
549    def handle_error(self, request, client_address):
550        """Override SocketServer.handle_error."""
551
552        self._logger.error(
553            'Exception in processing request from: %r\n%s',
554            client_address,
555            util.get_stack_trace())
556        # Note: client_address is a tuple.
557
558    def get_request(self):
559        """Override TCPServer.get_request to wrap OpenSSL.SSL.Connection
560        object with _StandaloneSSLConnection to provide makefile method. We
561        cannot substitute OpenSSL.SSL.Connection.makefile since it's readonly
562        attribute.
563        """
564
565        accepted_socket, client_address = self.socket.accept()
566
567        server_options = self.websocket_server_options
568        if server_options.use_tls:
569            if server_options.tls_module == _TLS_BY_STANDARD_MODULE:
570                try:
571                    accepted_socket.do_handshake()
572                except ssl.SSLError, e:
573                    self._logger.debug('%r', e)
574                    raise
575
576                # Print cipher in use. Handshake is done on accept.
577                self._logger.debug('Cipher: %s', accepted_socket.cipher())
578                self._logger.debug('Client cert: %r',
579                                   accepted_socket.getpeercert())
580            elif server_options.tls_module == _TLS_BY_PYOPENSSL:
581                # We cannot print the cipher in use. pyOpenSSL doesn't provide
582                # any method to fetch that.
583
584                ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
585                ctx.use_privatekey_file(server_options.private_key)
586                ctx.use_certificate_file(server_options.certificate)
587
588                def default_callback(conn, cert, errnum, errdepth, ok):
589                    return ok == 1
590
591                # See the OpenSSL document for SSL_CTX_set_verify.
592                if server_options.tls_client_auth:
593                    verify_mode = OpenSSL.SSL.VERIFY_PEER
594                    if not server_options.tls_client_cert_optional:
595                        verify_mode |= OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT
596                    ctx.set_verify(verify_mode, default_callback)
597                    ctx.load_verify_locations(server_options.tls_client_ca,
598                                              None)
599                else:
600                    ctx.set_verify(OpenSSL.SSL.VERIFY_NONE, default_callback)
601
602                accepted_socket = OpenSSL.SSL.Connection(ctx, accepted_socket)
603                accepted_socket.set_accept_state()
604
605                # Convert SSL related error into socket.error so that
606                # SocketServer ignores them and keeps running.
607                #
608                # TODO(tyoshino): Convert all kinds of errors.
609                try:
610                    accepted_socket.do_handshake()
611                except OpenSSL.SSL.Error, e:
612                    # Set errno part to 1 (SSL_ERROR_SSL) like the ssl module
613                    # does.
614                    self._logger.debug('%r', e)
615                    raise socket.error(1, '%r' % e)
616                cert = accepted_socket.get_peer_certificate()
617                if cert is not None:
618                    self._logger.debug('Client cert subject: %r',
619                                       cert.get_subject().get_components())
620                accepted_socket = _StandaloneSSLConnection(accepted_socket)
621            else:
622                raise ValueError('No TLS support module is available')
623
624        return accepted_socket, client_address
625
626    def serve_forever(self, poll_interval=0.5):
627        """Override SocketServer.BaseServer.serve_forever."""
628
629        self.__ws_serving = True
630        self.__ws_is_shut_down.clear()
631        handle_request = self.handle_request
632        if hasattr(self, '_handle_request_noblock'):
633            handle_request = self._handle_request_noblock
634        else:
635            self._logger.warning('Fallback to blocking request handler')
636        try:
637            while self.__ws_serving:
638                r, w, e = select.select(
639                    [socket_[0] for socket_ in self._sockets],
640                    [], [], poll_interval)
641                for socket_ in r:
642                    self.socket = socket_
643                    handle_request()
644                self.socket = None
645        finally:
646            self.__ws_is_shut_down.set()
647
648    def shutdown(self):
649        """Override SocketServer.BaseServer.shutdown."""
650
651        self.__ws_serving = False
652        self.__ws_is_shut_down.wait()
653
654
655class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
656    """CGIHTTPRequestHandler specialized for WebSocket."""
657
658    # Use httplib.HTTPMessage instead of mimetools.Message.
659    MessageClass = httplib.HTTPMessage
660
661    def setup(self):
662        """Override SocketServer.StreamRequestHandler.setup to wrap rfile
663        with MemorizingFile.
664
665        This method will be called by BaseRequestHandler's constructor
666        before calling BaseHTTPRequestHandler.handle.
667        BaseHTTPRequestHandler.handle will call
668        BaseHTTPRequestHandler.handle_one_request and it will call
669        WebSocketRequestHandler.parse_request.
670        """
671
672        # Call superclass's setup to prepare rfile, wfile, etc. See setup
673        # definition on the root class SocketServer.StreamRequestHandler to
674        # understand what this does.
675        CGIHTTPServer.CGIHTTPRequestHandler.setup(self)
676
677        self.rfile = memorizingfile.MemorizingFile(
678            self.rfile,
679            max_memorized_lines=_MAX_MEMORIZED_LINES)
680
681    def __init__(self, request, client_address, server):
682        self._logger = util.get_class_logger(self)
683
684        self._options = server.websocket_server_options
685
686        # Overrides CGIHTTPServerRequestHandler.cgi_directories.
687        self.cgi_directories = self._options.cgi_directories
688        # Replace CGIHTTPRequestHandler.is_executable method.
689        if self._options.is_executable_method is not None:
690            self.is_executable = self._options.is_executable_method
691
692        # This actually calls BaseRequestHandler.__init__.
693        CGIHTTPServer.CGIHTTPRequestHandler.__init__(
694            self, request, client_address, server)
695
696    def parse_request(self):
697        """Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request.
698
699        Return True to continue processing for HTTP(S), False otherwise.
700
701        See BaseHTTPRequestHandler.handle_one_request method which calls
702        this method to understand how the return value will be handled.
703        """
704
705        # We hook parse_request method, but also call the original
706        # CGIHTTPRequestHandler.parse_request since when we return False,
707        # CGIHTTPRequestHandler.handle_one_request continues processing and
708        # it needs variables set by CGIHTTPRequestHandler.parse_request.
709        #
710        # Variables set by this method will be also used by WebSocket request
711        # handling (self.path, self.command, self.requestline, etc. See also
712        # how _StandaloneRequest's members are implemented using these
713        # attributes).
714        if not CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self):
715            return False
716
717        if self._options.use_basic_auth:
718            auth = self.headers.getheader('Authorization')
719            if auth != self._options.basic_auth_credential:
720                self.send_response(401)
721                self.send_header('WWW-Authenticate',
722                                 'Basic realm="Pywebsocket"')
723                self.end_headers()
724                self._logger.info('Request basic authentication')
725                return True
726
727        host, port, resource = http_header_util.parse_uri(self.path)
728
729        # Special paths for XMLHttpRequest benchmark
730        xhr_benchmark_helper_prefix = '/073be001e10950692ccbf3a2ad21c245'
731        if resource == (xhr_benchmark_helper_prefix + '_send'):
732            xhr_benchmark_handler = XHRBenchmarkHandler(
733                self.headers, self.rfile, self.wfile)
734            xhr_benchmark_handler.do_send()
735            return False
736        if resource == (xhr_benchmark_helper_prefix + '_receive'):
737            xhr_benchmark_handler = XHRBenchmarkHandler(
738                self.headers, self.rfile, self.wfile)
739            xhr_benchmark_handler.do_receive()
740            return False
741
742        if resource is None:
743            self._logger.info('Invalid URI: %r', self.path)
744            self._logger.info('Fallback to CGIHTTPRequestHandler')
745            return True
746        server_options = self.server.websocket_server_options
747        if host is not None:
748            validation_host = server_options.validation_host
749            if validation_host is not None and host != validation_host:
750                self._logger.info('Invalid host: %r (expected: %r)',
751                                  host,
752                                  validation_host)
753                self._logger.info('Fallback to CGIHTTPRequestHandler')
754                return True
755        if port is not None:
756            validation_port = server_options.validation_port
757            if validation_port is not None and port != validation_port:
758                self._logger.info('Invalid port: %r (expected: %r)',
759                                  port,
760                                  validation_port)
761                self._logger.info('Fallback to CGIHTTPRequestHandler')
762                return True
763        self.path = resource
764
765        request = _StandaloneRequest(self, self._options.use_tls)
766
767        try:
768            # Fallback to default http handler for request paths for which
769            # we don't have request handlers.
770            if not self._options.dispatcher.get_handler_suite(self.path):
771                self._logger.info('No handler for resource: %r',
772                                  self.path)
773                self._logger.info('Fallback to CGIHTTPRequestHandler')
774                return True
775        except dispatch.DispatchException, e:
776            self._logger.info('Dispatch failed for error: %s', e)
777            self.send_error(e.status)
778            return False
779
780        # If any Exceptions without except clause setup (including
781        # DispatchException) is raised below this point, it will be caught
782        # and logged by WebSocketServer.
783
784        try:
785            try:
786                handshake.do_handshake(
787                    request,
788                    self._options.dispatcher,
789                    allowDraft75=self._options.allow_draft75,
790                    strict=self._options.strict)
791            except handshake.VersionException, e:
792                self._logger.info('Handshake failed for version error: %s', e)
793                self.send_response(common.HTTP_STATUS_BAD_REQUEST)
794                self.send_header(common.SEC_WEBSOCKET_VERSION_HEADER,
795                                 e.supported_versions)
796                self.end_headers()
797                return False
798            except handshake.HandshakeException, e:
799                # Handshake for ws(s) failed.
800                self._logger.info('Handshake failed for error: %s', e)
801                self.send_error(e.status)
802                return False
803
804            request._dispatcher = self._options.dispatcher
805            self._options.dispatcher.transfer_data(request)
806        except handshake.AbortedByUserException, e:
807            self._logger.info('Aborted: %s', e)
808        return False
809
810    def log_request(self, code='-', size='-'):
811        """Override BaseHTTPServer.log_request."""
812
813        self._logger.info('"%s" %s %s',
814                          self.requestline, str(code), str(size))
815
816    def log_error(self, *args):
817        """Override BaseHTTPServer.log_error."""
818
819        # Despite the name, this method is for warnings than for errors.
820        # For example, HTTP status code is logged by this method.
821        self._logger.warning('%s - %s',
822                             self.address_string(),
823                             args[0] % args[1:])
824
825    def is_cgi(self):
826        """Test whether self.path corresponds to a CGI script.
827
828        Add extra check that self.path doesn't contains ..
829        Also check if the file is a executable file or not.
830        If the file is not executable, it is handled as static file or dir
831        rather than a CGI script.
832        """
833
834        if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self):
835            if '..' in self.path:
836                return False
837            # strip query parameter from request path
838            resource_name = self.path.split('?', 2)[0]
839            # convert resource_name into real path name in filesystem.
840            scriptfile = self.translate_path(resource_name)
841            if not os.path.isfile(scriptfile):
842                return False
843            if not self.is_executable(scriptfile):
844                return False
845            return True
846        return False
847
848
849def _get_logger_from_class(c):
850    return logging.getLogger('%s.%s' % (c.__module__, c.__name__))
851
852
853def _configure_logging(options):
854    logging.addLevelName(common.LOGLEVEL_FINE, 'FINE')
855
856    logger = logging.getLogger()
857    logger.setLevel(logging.getLevelName(options.log_level.upper()))
858    if options.log_file:
859        handler = logging.handlers.RotatingFileHandler(
860                options.log_file, 'a', options.log_max, options.log_count)
861    else:
862        handler = logging.StreamHandler()
863    formatter = logging.Formatter(
864            '[%(asctime)s] [%(levelname)s] %(name)s: %(message)s')
865    handler.setFormatter(formatter)
866    logger.addHandler(handler)
867
868    deflate_log_level_name = logging.getLevelName(
869        options.deflate_log_level.upper())
870    _get_logger_from_class(util._Deflater).setLevel(
871        deflate_log_level_name)
872    _get_logger_from_class(util._Inflater).setLevel(
873        deflate_log_level_name)
874
875
876def _build_option_parser():
877    parser = optparse.OptionParser()
878
879    parser.add_option('--config', dest='config_file', type='string',
880                      default=None,
881                      help=('Path to configuration file. See the file comment '
882                            'at the top of this file for the configuration '
883                            'file format'))
884    parser.add_option('-H', '--server-host', '--server_host',
885                      dest='server_host',
886                      default='',
887                      help='server hostname to listen to')
888    parser.add_option('-V', '--validation-host', '--validation_host',
889                      dest='validation_host',
890                      default=None,
891                      help='server hostname to validate in absolute path.')
892    parser.add_option('-p', '--port', dest='port', type='int',
893                      default=common.DEFAULT_WEB_SOCKET_PORT,
894                      help='port to listen to')
895    parser.add_option('-P', '--validation-port', '--validation_port',
896                      dest='validation_port', type='int',
897                      default=None,
898                      help='server port to validate in absolute path.')
899    parser.add_option('-w', '--websock-handlers', '--websock_handlers',
900                      dest='websock_handlers',
901                      default='.',
902                      help=('The root directory of WebSocket handler files. '
903                            'If the path is relative, --document-root is used '
904                            'as the base.'))
905    parser.add_option('-m', '--websock-handlers-map-file',
906                      '--websock_handlers_map_file',
907                      dest='websock_handlers_map_file',
908                      default=None,
909                      help=('WebSocket handlers map file. '
910                            'Each line consists of alias_resource_path and '
911                            'existing_resource_path, separated by spaces.'))
912    parser.add_option('-s', '--scan-dir', '--scan_dir', dest='scan_dir',
913                      default=None,
914                      help=('Must be a directory under --websock-handlers. '
915                            'Only handlers under this directory are scanned '
916                            'and registered to the server. '
917                            'Useful for saving scan time when the handler '
918                            'root directory contains lots of files that are '
919                            'not handler file or are handler files but you '
920                            'don\'t want them to be registered. '))
921    parser.add_option('--allow-handlers-outside-root-dir',
922                      '--allow_handlers_outside_root_dir',
923                      dest='allow_handlers_outside_root_dir',
924                      action='store_true',
925                      default=False,
926                      help=('Scans WebSocket handlers even if their canonical '
927                            'path is not under --websock-handlers.'))
928    parser.add_option('-d', '--document-root', '--document_root',
929                      dest='document_root', default='.',
930                      help='Document root directory.')
931    parser.add_option('-x', '--cgi-paths', '--cgi_paths', dest='cgi_paths',
932                      default=None,
933                      help=('CGI paths relative to document_root.'
934                            'Comma-separated. (e.g -x /cgi,/htbin) '
935                            'Files under document_root/cgi_path are handled '
936                            'as CGI programs. Must be executable.'))
937    parser.add_option('-t', '--tls', dest='use_tls', action='store_true',
938                      default=False, help='use TLS (wss://)')
939    parser.add_option('--tls-module', '--tls_module', dest='tls_module',
940                      type='choice',
941                      choices = [_TLS_BY_STANDARD_MODULE, _TLS_BY_PYOPENSSL],
942                      help='Use ssl module if "%s" is specified. '
943                      'Use pyOpenSSL module if "%s" is specified' %
944                      (_TLS_BY_STANDARD_MODULE, _TLS_BY_PYOPENSSL))
945    parser.add_option('-k', '--private-key', '--private_key',
946                      dest='private_key',
947                      default='', help='TLS private key file.')
948    parser.add_option('-c', '--certificate', dest='certificate',
949                      default='', help='TLS certificate file.')
950    parser.add_option('--tls-client-auth', dest='tls_client_auth',
951                      action='store_true', default=False,
952                      help='Requests TLS client auth on every connection.')
953    parser.add_option('--tls-client-cert-optional',
954                      dest='tls_client_cert_optional',
955                      action='store_true', default=False,
956                      help=('Makes client certificate optional even though '
957                            'TLS client auth is enabled.'))
958    parser.add_option('--tls-client-ca', dest='tls_client_ca', default='',
959                      help=('Specifies a pem file which contains a set of '
960                            'concatenated CA certificates which are used to '
961                            'validate certificates passed from clients'))
962    parser.add_option('--basic-auth', dest='use_basic_auth',
963                      action='store_true', default=False,
964                      help='Requires Basic authentication.')
965    parser.add_option('--basic-auth-credential',
966                      dest='basic_auth_credential', default='test:test',
967                      help='Specifies the credential of basic authentication '
968                      'by username:password pair (e.g. test:test).')
969    parser.add_option('-l', '--log-file', '--log_file', dest='log_file',
970                      default='', help='Log file.')
971    # Custom log level:
972    # - FINE: Prints status of each frame processing step
973    parser.add_option('--log-level', '--log_level', type='choice',
974                      dest='log_level', default='warn',
975                      choices=['fine',
976                               'debug', 'info', 'warning', 'warn', 'error',
977                               'critical'],
978                      help='Log level.')
979    parser.add_option('--deflate-log-level', '--deflate_log_level',
980                      type='choice',
981                      dest='deflate_log_level', default='warn',
982                      choices=['debug', 'info', 'warning', 'warn', 'error',
983                               'critical'],
984                      help='Log level for _Deflater and _Inflater.')
985    parser.add_option('--thread-monitor-interval-in-sec',
986                      '--thread_monitor_interval_in_sec',
987                      dest='thread_monitor_interval_in_sec',
988                      type='int', default=-1,
989                      help=('If positive integer is specified, run a thread '
990                            'monitor to show the status of server threads '
991                            'periodically in the specified inteval in '
992                            'second. If non-positive integer is specified, '
993                            'disable the thread monitor.'))
994    parser.add_option('--log-max', '--log_max', dest='log_max', type='int',
995                      default=_DEFAULT_LOG_MAX_BYTES,
996                      help='Log maximum bytes')
997    parser.add_option('--log-count', '--log_count', dest='log_count',
998                      type='int', default=_DEFAULT_LOG_BACKUP_COUNT,
999                      help='Log backup count')
1000    parser.add_option('--allow-draft75', dest='allow_draft75',
1001                      action='store_true', default=False,
1002                      help='Obsolete option. Ignored.')
1003    parser.add_option('--strict', dest='strict', action='store_true',
1004                      default=False, help='Obsolete option. Ignored.')
1005    parser.add_option('-q', '--queue', dest='request_queue_size', type='int',
1006                      default=_DEFAULT_REQUEST_QUEUE_SIZE,
1007                      help='request queue size')
1008
1009    return parser
1010
1011
1012class ThreadMonitor(threading.Thread):
1013    daemon = True
1014
1015    def __init__(self, interval_in_sec):
1016        threading.Thread.__init__(self, name='ThreadMonitor')
1017
1018        self._logger = util.get_class_logger(self)
1019
1020        self._interval_in_sec = interval_in_sec
1021
1022    def run(self):
1023        while True:
1024            thread_name_list = []
1025            for thread in threading.enumerate():
1026                thread_name_list.append(thread.name)
1027            self._logger.info(
1028                "%d active threads: %s",
1029                threading.active_count(),
1030                ', '.join(thread_name_list))
1031            time.sleep(self._interval_in_sec)
1032
1033
1034def _parse_args_and_config(args):
1035    parser = _build_option_parser()
1036
1037    # First, parse options without configuration file.
1038    temporary_options, temporary_args = parser.parse_args(args=args)
1039    if temporary_args:
1040        logging.critical(
1041            'Unrecognized positional arguments: %r', temporary_args)
1042        sys.exit(1)
1043
1044    if temporary_options.config_file:
1045        try:
1046            config_fp = open(temporary_options.config_file, 'r')
1047        except IOError, e:
1048            logging.critical(
1049                'Failed to open configuration file %r: %r',
1050                temporary_options.config_file,
1051                e)
1052            sys.exit(1)
1053
1054        config_parser = ConfigParser.SafeConfigParser()
1055        config_parser.readfp(config_fp)
1056        config_fp.close()
1057
1058        args_from_config = []
1059        for name, value in config_parser.items('pywebsocket'):
1060            args_from_config.append('--' + name)
1061            args_from_config.append(value)
1062        if args is None:
1063            args = args_from_config
1064        else:
1065            args = args_from_config + args
1066        return parser.parse_args(args=args)
1067    else:
1068        return temporary_options, temporary_args
1069
1070
1071def _main(args=None):
1072    """You can call this function from your own program, but please note that
1073    this function has some side-effects that might affect your program. For
1074    example, util.wrap_popen3_for_win use in this method replaces implementation
1075    of os.popen3.
1076    """
1077
1078    options, args = _parse_args_and_config(args=args)
1079
1080    os.chdir(options.document_root)
1081
1082    _configure_logging(options)
1083
1084    if options.allow_draft75:
1085        logging.warning('--allow_draft75 option is obsolete.')
1086
1087    if options.strict:
1088        logging.warning('--strict option is obsolete.')
1089
1090    # TODO(tyoshino): Clean up initialization of CGI related values. Move some
1091    # of code here to WebSocketRequestHandler class if it's better.
1092    options.cgi_directories = []
1093    options.is_executable_method = None
1094    if options.cgi_paths:
1095        options.cgi_directories = options.cgi_paths.split(',')
1096        if sys.platform in ('cygwin', 'win32'):
1097            cygwin_path = None
1098            # For Win32 Python, it is expected that CYGWIN_PATH
1099            # is set to a directory of cygwin binaries.
1100            # For example, websocket_server.py in Chromium sets CYGWIN_PATH to
1101            # full path of third_party/cygwin/bin.
1102            if 'CYGWIN_PATH' in os.environ:
1103                cygwin_path = os.environ['CYGWIN_PATH']
1104            util.wrap_popen3_for_win(cygwin_path)
1105
1106            def __check_script(scriptpath):
1107                return util.get_script_interp(scriptpath, cygwin_path)
1108
1109            options.is_executable_method = __check_script
1110
1111    if options.use_tls:
1112        if options.tls_module is None:
1113            if _import_ssl():
1114                options.tls_module = _TLS_BY_STANDARD_MODULE
1115                logging.debug('Using ssl module')
1116            elif _import_pyopenssl():
1117                options.tls_module = _TLS_BY_PYOPENSSL
1118                logging.debug('Using pyOpenSSL module')
1119            else:
1120                logging.critical(
1121                        'TLS support requires ssl or pyOpenSSL module.')
1122                sys.exit(1)
1123        elif options.tls_module == _TLS_BY_STANDARD_MODULE:
1124            if not _import_ssl():
1125                logging.critical('ssl module is not available')
1126                sys.exit(1)
1127        elif options.tls_module == _TLS_BY_PYOPENSSL:
1128            if not _import_pyopenssl():
1129                logging.critical('pyOpenSSL module is not available')
1130                sys.exit(1)
1131        else:
1132            logging.critical('Invalid --tls-module option: %r',
1133                             options.tls_module)
1134            sys.exit(1)
1135
1136        if not options.private_key or not options.certificate:
1137            logging.critical(
1138                    'To use TLS, specify private_key and certificate.')
1139            sys.exit(1)
1140
1141        if (options.tls_client_cert_optional and
1142            not options.tls_client_auth):
1143            logging.critical('Client authentication must be enabled to '
1144                             'specify tls_client_cert_optional')
1145            sys.exit(1)
1146    else:
1147        if options.tls_module is not None:
1148            logging.critical('Use --tls-module option only together with '
1149                             '--use-tls option.')
1150            sys.exit(1)
1151
1152        if options.tls_client_auth:
1153            logging.critical('TLS must be enabled for client authentication.')
1154            sys.exit(1)
1155
1156        if options.tls_client_cert_optional:
1157            logging.critical('TLS must be enabled for client authentication.')
1158            sys.exit(1)
1159
1160    if not options.scan_dir:
1161        options.scan_dir = options.websock_handlers
1162
1163    if options.use_basic_auth:
1164        options.basic_auth_credential = 'Basic ' + base64.b64encode(
1165            options.basic_auth_credential)
1166
1167    try:
1168        if options.thread_monitor_interval_in_sec > 0:
1169            # Run a thread monitor to show the status of server threads for
1170            # debugging.
1171            ThreadMonitor(options.thread_monitor_interval_in_sec).start()
1172
1173        server = WebSocketServer(options)
1174        server.serve_forever()
1175    except Exception, e:
1176        logging.critical('mod_pywebsocket: %s' % e)
1177        logging.critical('mod_pywebsocket: %s' % util.get_stack_trace())
1178        sys.exit(1)
1179
1180
1181if __name__ == '__main__':
1182    _main(sys.argv[1:])
1183
1184
1185# vi:sts=4 sw=4 et
1186