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