• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Copyright 2009, 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 Web Socket server.
34
35Use this server to run mod_pywebsocket without Apache HTTP Server.
36
37Usage:
38    python standalone.py [-p <ws_port>] [-w <websock_handlers>]
39                         [-s <scan_dir>]
40                         [-d <document_root>]
41                         [-m <websock_handlers_map_file>]
42                         ... for other options, see _main below ...
43
44<ws_port> is the port number to use for ws:// connection.
45
46<document_root> is the path to the root directory of HTML files.
47
48<websock_handlers> is the path to the root directory of Web Socket handlers.
49See __init__.py for details of <websock_handlers> and how to write Web Socket
50handlers. If this path is relative, <document_root> is used as the base.
51
52<scan_dir> is a path under the root directory. If specified, only the handlers
53under scan_dir are scanned. This is useful in saving scan time.
54
55Note:
56This server is derived from SocketServer.ThreadingMixIn. Hence a thread is
57used for each request.
58"""
59
60import BaseHTTPServer
61import CGIHTTPServer
62import SimpleHTTPServer
63import SocketServer
64import logging
65import logging.handlers
66import optparse
67import os
68import re
69import socket
70import sys
71
72_HAS_OPEN_SSL = False
73try:
74    import OpenSSL.SSL
75    _HAS_OPEN_SSL = True
76except ImportError:
77    pass
78
79import dispatch
80import handshake
81import memorizingfile
82import util
83
84
85_LOG_LEVELS = {
86    'debug': logging.DEBUG,
87    'info': logging.INFO,
88    'warn': logging.WARN,
89    'error': logging.ERROR,
90    'critical': logging.CRITICAL};
91
92_DEFAULT_LOG_MAX_BYTES = 1024 * 256
93_DEFAULT_LOG_BACKUP_COUNT = 5
94
95_DEFAULT_REQUEST_QUEUE_SIZE = 128
96
97# 1024 is practically large enough to contain WebSocket handshake lines.
98_MAX_MEMORIZED_LINES = 1024
99
100def _print_warnings_if_any(dispatcher):
101    warnings = dispatcher.source_warnings()
102    if warnings:
103        for warning in warnings:
104            logging.warning('mod_pywebsocket: %s' % warning)
105
106
107class _StandaloneConnection(object):
108    """Mimic mod_python mp_conn."""
109
110    def __init__(self, request_handler):
111        """Construct an instance.
112
113        Args:
114            request_handler: A WebSocketRequestHandler instance.
115        """
116        self._request_handler = request_handler
117
118    def get_local_addr(self):
119        """Getter to mimic mp_conn.local_addr."""
120        return (self._request_handler.server.server_name,
121                self._request_handler.server.server_port)
122    local_addr = property(get_local_addr)
123
124    def get_remote_addr(self):
125        """Getter to mimic mp_conn.remote_addr.
126
127        Setting the property in __init__ won't work because the request
128        handler is not initialized yet there."""
129        return self._request_handler.client_address
130    remote_addr = property(get_remote_addr)
131
132    def write(self, data):
133        """Mimic mp_conn.write()."""
134        return self._request_handler.wfile.write(data)
135
136    def read(self, length):
137        """Mimic mp_conn.read()."""
138        return self._request_handler.rfile.read(length)
139
140    def get_memorized_lines(self):
141        """Get memorized lines."""
142        return self._request_handler.rfile.get_memorized_lines()
143
144
145class _StandaloneRequest(object):
146    """Mimic mod_python request."""
147
148    def __init__(self, request_handler, use_tls):
149        """Construct an instance.
150
151        Args:
152            request_handler: A WebSocketRequestHandler instance.
153        """
154        self._request_handler = request_handler
155        self.connection = _StandaloneConnection(request_handler)
156        self._use_tls = use_tls
157
158    def get_uri(self):
159        """Getter to mimic request.uri."""
160        return self._request_handler.path
161    uri = property(get_uri)
162
163    def get_headers_in(self):
164        """Getter to mimic request.headers_in."""
165        return self._request_handler.headers
166    headers_in = property(get_headers_in)
167
168    def is_https(self):
169        """Mimic request.is_https()."""
170        return self._use_tls
171
172
173class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
174    """HTTPServer specialized for Web Socket."""
175
176    SocketServer.ThreadingMixIn.daemon_threads = True
177
178    def __init__(self, server_address, RequestHandlerClass):
179        """Override SocketServer.BaseServer.__init__."""
180
181        SocketServer.BaseServer.__init__(
182                self, server_address, RequestHandlerClass)
183        self.socket = self._create_socket()
184        self.server_bind()
185        self.server_activate()
186
187    def _create_socket(self):
188        socket_ = socket.socket(self.address_family, self.socket_type)
189        if WebSocketServer.options.use_tls:
190            ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
191            ctx.use_privatekey_file(WebSocketServer.options.private_key)
192            ctx.use_certificate_file(WebSocketServer.options.certificate)
193            socket_ = OpenSSL.SSL.Connection(ctx, socket_)
194        return socket_
195
196    def handle_error(self, rquest, client_address):
197        """Override SocketServer.handle_error."""
198
199        logging.error(
200            ('Exception in processing request from: %r' % (client_address,)) +
201            '\n' + util.get_stack_trace())
202        # Note: client_address is a tuple. To match it against %r, we need the
203        # trailing comma.
204
205
206class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
207    """CGIHTTPRequestHandler specialized for Web Socket."""
208
209    def setup(self):
210        """Override SocketServer.StreamRequestHandler.setup."""
211
212        self.connection = self.request
213        self.rfile = memorizingfile.MemorizingFile(
214                socket._fileobject(self.request, 'rb', self.rbufsize),
215                max_memorized_lines=_MAX_MEMORIZED_LINES)
216        self.wfile = socket._fileobject(self.request, 'wb', self.wbufsize)
217
218    def __init__(self, *args, **keywords):
219        self._request = _StandaloneRequest(
220                self, WebSocketRequestHandler.options.use_tls)
221        self._dispatcher = WebSocketRequestHandler.options.dispatcher
222        self._print_warnings_if_any()
223        self._handshaker = handshake.Handshaker(
224                self._request, self._dispatcher,
225                WebSocketRequestHandler.options.strict)
226        CGIHTTPServer.CGIHTTPRequestHandler.__init__(
227                self, *args, **keywords)
228
229    def _print_warnings_if_any(self):
230        warnings = self._dispatcher.source_warnings()
231        if warnings:
232            for warning in warnings:
233                logging.warning('mod_pywebsocket: %s' % warning)
234
235    def parse_request(self):
236        """Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request.
237
238        Return True to continue processing for HTTP(S), False otherwise.
239        """
240        result = CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self)
241        if result:
242            try:
243                self._handshaker.do_handshake()
244                self._dispatcher.transfer_data(self._request)
245                return False
246            except handshake.HandshakeError, e:
247                # Handshake for ws(s) failed. Assume http(s).
248                logging.info('mod_pywebsocket: %s' % e)
249                return True
250            except dispatch.DispatchError, e:
251                logging.warning('mod_pywebsocket: %s' % e)
252                return False
253            except Exception, e:
254                logging.warning('mod_pywebsocket: %s' % e)
255                logging.info('mod_pywebsocket: %s' % util.get_stack_trace())
256                return False
257        return result
258
259    def log_request(self, code='-', size='-'):
260        """Override BaseHTTPServer.log_request."""
261
262        logging.info('"%s" %s %s',
263                     self.requestline, str(code), str(size))
264
265    def log_error(self, *args):
266        """Override BaseHTTPServer.log_error."""
267
268        # Despite the name, this method is for warnings than for errors.
269        # For example, HTTP status code is logged by this method.
270        logging.warn('%s - %s' % (self.address_string(), (args[0] % args[1:])))
271
272    def is_cgi(self):
273        """Test whether self.path corresponds to a CGI script.
274
275        Add extra check that self.path doesn't contains .."""
276        if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self):
277            if '..' in self.path:
278                return False
279            return True
280        return False
281
282
283def _configure_logging(options):
284    logger = logging.getLogger()
285    logger.setLevel(_LOG_LEVELS[options.log_level])
286    if options.log_file:
287        handler = logging.handlers.RotatingFileHandler(
288                options.log_file, 'a', options.log_max, options.log_count)
289    else:
290        handler = logging.StreamHandler()
291    formatter = logging.Formatter(
292            "[%(asctime)s] [%(levelname)s] %(name)s: %(message)s")
293    handler.setFormatter(formatter)
294    logger.addHandler(handler)
295
296def _alias_handlers(dispatcher, websock_handlers_map_file):
297    """Set aliases specified in websock_handler_map_file in dispatcher.
298
299    Args:
300        dispatcher: dispatch.Dispatcher instance
301        websock_handler_map_file: alias map file
302    """
303    fp = open(websock_handlers_map_file)
304    try:
305        for line in fp:
306            if line[0] == '#' or line.isspace():
307                continue
308            m = re.match('(\S+)\s+(\S+)', line)
309            if not m:
310                logging.warning('Wrong format in map file:' + line)
311                continue
312            try:
313                dispatcher.add_resource_path_alias(
314                    m.group(1), m.group(2))
315            except dispatch.DispatchError, e:
316                logging.error(str(e))
317    finally:
318        fp.close()
319
320
321
322def _main():
323    parser = optparse.OptionParser()
324    parser.add_option('-p', '--port', dest='port', type='int',
325                      default=handshake._DEFAULT_WEB_SOCKET_PORT,
326                      help='port to listen to')
327    parser.add_option('-w', '--websock_handlers', dest='websock_handlers',
328                      default='.',
329                      help='Web Socket handlers root directory.')
330    parser.add_option('-m', '--websock_handlers_map_file',
331                      dest='websock_handlers_map_file',
332                      default=None,
333                      help=('Web Socket handlers map file. '
334                            'Each line consists of alias_resource_path and '
335                            'existing_resource_path, separated by spaces.'))
336    parser.add_option('-s', '--scan_dir', dest='scan_dir',
337                      default=None,
338                      help=('Web Socket handlers scan directory. '
339                            'Must be a directory under websock_handlers.'))
340    parser.add_option('-d', '--document_root', dest='document_root',
341                      default='.',
342                      help='Document root directory.')
343    parser.add_option('-x', '--cgi_paths', dest='cgi_paths',
344                      default=None,
345                      help=('CGI paths relative to document_root.'
346                            'Comma-separated. (e.g -x /cgi,/htbin) '
347                            'Files under document_root/cgi_path are handled '
348                            'as CGI programs. Must be executable.'))
349    parser.add_option('-t', '--tls', dest='use_tls', action='store_true',
350                      default=False, help='use TLS (wss://)')
351    parser.add_option('-k', '--private_key', dest='private_key',
352                      default='', help='TLS private key file.')
353    parser.add_option('-c', '--certificate', dest='certificate',
354                      default='', help='TLS certificate file.')
355    parser.add_option('-l', '--log_file', dest='log_file',
356                      default='', help='Log file.')
357    parser.add_option('--log_level', type='choice', dest='log_level',
358                      default='warn',
359                      choices=['debug', 'info', 'warn', 'error', 'critical'],
360                      help='Log level.')
361    parser.add_option('--log_max', dest='log_max', type='int',
362                      default=_DEFAULT_LOG_MAX_BYTES,
363                      help='Log maximum bytes')
364    parser.add_option('--log_count', dest='log_count', type='int',
365                      default=_DEFAULT_LOG_BACKUP_COUNT,
366                      help='Log backup count')
367    parser.add_option('--strict', dest='strict', action='store_true',
368                      default=False, help='Strictly check handshake request')
369    parser.add_option('-q', '--queue', dest='request_queue_size', type='int',
370                      default=_DEFAULT_REQUEST_QUEUE_SIZE,
371                      help='request queue size')
372    options = parser.parse_args()[0]
373
374    os.chdir(options.document_root)
375
376    _configure_logging(options)
377
378    SocketServer.TCPServer.request_queue_size = options.request_queue_size
379    CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = []
380
381    if options.cgi_paths:
382        CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = \
383            options.cgi_paths.split(',')
384
385    if options.use_tls:
386        if not _HAS_OPEN_SSL:
387            logging.critical('To use TLS, install pyOpenSSL.')
388            sys.exit(1)
389        if not options.private_key or not options.certificate:
390            logging.critical(
391                    'To use TLS, specify private_key and certificate.')
392            sys.exit(1)
393
394    if not options.scan_dir:
395        options.scan_dir = options.websock_handlers
396
397    try:
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(options.websock_handlers,
401                                                 options.scan_dir)
402        if options.websock_handlers_map_file:
403            _alias_handlers(options.dispatcher,
404                            options.websock_handlers_map_file)
405        _print_warnings_if_any(options.dispatcher)
406
407        WebSocketRequestHandler.options = options
408        WebSocketServer.options = options
409
410        server = WebSocketServer(('', options.port), WebSocketRequestHandler)
411        server.serve_forever()
412    except Exception, e:
413        logging.critical(str(e))
414        sys.exit(1)
415
416
417if __name__ == '__main__':
418    _main()
419
420
421# vi:sts=4 sw=4 et
422