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