standalone.py (18871B)
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 """Standalone WebSocket server. 32 33 Use this file to launch pywebsocket as a standalone server. 34 35 36 BASIC USAGE 37 =========== 38 39 Go to the src directory and run 40 41 $ python mod_pywebsocket/standalone.py [-p <ws_port>] 42 [-w <websock_handlers>] 43 [-d <document_root>] 44 45 <ws_port> is the port number to use for ws:// connection. 46 47 <document_root> is the path to the root directory of HTML files. 48 49 <websock_handlers> is the path to the root directory of WebSocket handlers. 50 If not specified, <document_root> will be used. See __init__.py (or 51 run $ pydoc mod_pywebsocket) for how to write WebSocket handlers. 52 53 For more detail and other options, run 54 55 $ python mod_pywebsocket/standalone.py --help 56 57 or see _build_option_parser method below. 58 59 For trouble shooting, adding "--log_level debug" might help you. 60 61 62 TRY DEMO 63 ======== 64 65 Go to the src directory and run standalone.py with -d option to set the 66 document root to the directory containing example HTMLs and handlers like this: 67 68 $ cd src 69 $ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example 70 71 to launch pywebsocket with the sample handler and html on port 80. Open 72 http://localhost/console.html, click the connect button, type something into 73 the text box next to the send button and click the send button. If everything 74 is working, you'll see the message you typed echoed by the server. 75 76 77 USING TLS 78 ========= 79 80 To run the standalone server with TLS support, run it with -t, -k, and -c 81 options. When TLS is enabled, the standalone server accepts only TLS connection. 82 83 Note that when ssl module is used and the key/cert location is incorrect, 84 TLS connection silently fails while pyOpenSSL fails on startup. 85 86 Example: 87 88 $ PYTHONPATH=. python mod_pywebsocket/standalone.py \ 89 -d example \ 90 -p 10443 \ 91 -t \ 92 -c ../test/cert/cert.pem \ 93 -k ../test/cert/key.pem \ 94 95 Note that when passing a relative path to -c and -k option, it will be resolved 96 using the document root directory as the base. 97 98 99 USING CLIENT AUTHENTICATION 100 =========================== 101 102 To run the standalone server with TLS client authentication support, run it with 103 --tls-client-auth and --tls-client-ca options in addition to ones required for 104 TLS support. 105 106 Example: 107 108 $ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example -p 10443 -t \ 109 -c ../test/cert/cert.pem -k ../test/cert/key.pem \ 110 --tls-client-auth \ 111 --tls-client-ca=../test/cert/cacert.pem 112 113 Note that when passing a relative path to --tls-client-ca option, it will be 114 resolved using the document root directory as the base. 115 116 117 CONFIGURATION FILE 118 ================== 119 120 You can also write a configuration file and use it by specifying the path to 121 the configuration file by --config option. Please write a configuration file 122 following the documentation of the Python ConfigParser library. Name of each 123 entry must be the long version argument name. E.g. to set log level to debug, 124 add the following line: 125 126 log_level=debug 127 128 For options which doesn't take value, please add some fake value. E.g. for 129 --tls option, add the following line: 130 131 tls=True 132 133 Note that tls will be enabled even if you write tls=False as the value part is 134 fake. 135 136 When both a command line argument and a configuration file entry are set for 137 the same configuration item, the command line value will override one in the 138 configuration file. 139 140 141 THREADING 142 ========= 143 144 This server is derived from SocketServer.ThreadingMixIn. Hence a thread is 145 used for each request. 146 147 148 SECURITY WARNING 149 ================ 150 151 This uses CGIHTTPServer and CGIHTTPServer is not secure. 152 It may execute arbitrary Python code or external programs. It should not be 153 used outside a firewall. 154 """ 155 156 from __future__ import absolute_import 157 from six.moves import configparser 158 import base64 159 import logging 160 import argparse 161 import os 162 import six 163 import sys 164 import traceback 165 166 from mod_pywebsocket import common 167 from mod_pywebsocket import util 168 from mod_pywebsocket import server_util 169 from mod_pywebsocket.websocket_server import WebSocketServer 170 171 _DEFAULT_LOG_MAX_BYTES = 1024 * 256 172 _DEFAULT_LOG_BACKUP_COUNT = 5 173 174 _DEFAULT_REQUEST_QUEUE_SIZE = 128 175 176 177 def _build_option_parser(): 178 parser = argparse.ArgumentParser() 179 180 parser.add_argument( 181 '--config', 182 dest='config_file', 183 type=six.text_type, 184 default=None, 185 help=('Path to configuration file. See the file comment ' 186 'at the top of this file for the configuration ' 187 'file format')) 188 parser.add_argument('-H', 189 '--server-host', 190 '--server_host', 191 dest='server_host', 192 default='', 193 help='server hostname to listen to') 194 parser.add_argument('-V', 195 '--validation-host', 196 '--validation_host', 197 dest='validation_host', 198 default=None, 199 help='server hostname to validate in absolute path.') 200 parser.add_argument('-p', 201 '--port', 202 dest='port', 203 type=int, 204 default=common.DEFAULT_WEB_SOCKET_PORT, 205 help='port to listen to') 206 parser.add_argument('-P', 207 '--validation-port', 208 '--validation_port', 209 dest='validation_port', 210 type=int, 211 default=None, 212 help='server port to validate in absolute path.') 213 parser.add_argument( 214 '-w', 215 '--websock-handlers', 216 '--websock_handlers', 217 dest='websock_handlers', 218 default='.', 219 help=('The root directory of WebSocket handler files. ' 220 'If the path is relative, --document-root is used ' 221 'as the base.')) 222 parser.add_argument('-m', 223 '--websock-handlers-map-file', 224 '--websock_handlers_map_file', 225 dest='websock_handlers_map_file', 226 default=None, 227 help=('WebSocket handlers map file. ' 228 'Each line consists of alias_resource_path and ' 229 'existing_resource_path, separated by spaces.')) 230 parser.add_argument('-s', 231 '--scan-dir', 232 '--scan_dir', 233 dest='scan_dir', 234 default=None, 235 help=('Must be a directory under --websock-handlers. ' 236 'Only handlers under this directory are scanned ' 237 'and registered to the server. ' 238 'Useful for saving scan time when the handler ' 239 'root directory contains lots of files that are ' 240 'not handler file or are handler files but you ' 241 'don\'t want them to be registered. ')) 242 parser.add_argument( 243 '--allow-handlers-outside-root-dir', 244 '--allow_handlers_outside_root_dir', 245 dest='allow_handlers_outside_root_dir', 246 action='store_true', 247 default=False, 248 help=('Scans WebSocket handlers even if their canonical ' 249 'path is not under --websock-handlers.')) 250 parser.add_argument('-d', 251 '--document-root', 252 '--document_root', 253 dest='document_root', 254 default='.', 255 help='Document root directory.') 256 parser.add_argument('-x', 257 '--cgi-paths', 258 '--cgi_paths', 259 dest='cgi_paths', 260 default=None, 261 help=('CGI paths relative to document_root.' 262 'Comma-separated. (e.g -x /cgi,/htbin) ' 263 'Files under document_root/cgi_path are handled ' 264 'as CGI programs. Must be executable.')) 265 parser.add_argument('-t', 266 '--tls', 267 dest='use_tls', 268 action='store_true', 269 default=False, 270 help='use TLS (wss://)') 271 parser.add_argument('-k', 272 '--private-key', 273 '--private_key', 274 dest='private_key', 275 default='', 276 help='TLS private key file.') 277 parser.add_argument('-c', 278 '--certificate', 279 dest='certificate', 280 default='', 281 help='TLS certificate file.') 282 parser.add_argument('--tls-client-auth', 283 dest='tls_client_auth', 284 action='store_true', 285 default=False, 286 help='Requests TLS client auth on every connection.') 287 parser.add_argument('--tls-client-cert-optional', 288 dest='tls_client_cert_optional', 289 action='store_true', 290 default=False, 291 help=('Makes client certificate optional even though ' 292 'TLS client auth is enabled.')) 293 parser.add_argument('--tls-client-ca', 294 dest='tls_client_ca', 295 default='', 296 help=('Specifies a pem file which contains a set of ' 297 'concatenated CA certificates which are used to ' 298 'validate certificates passed from clients')) 299 parser.add_argument('--basic-auth', 300 dest='use_basic_auth', 301 action='store_true', 302 default=False, 303 help='Requires Basic authentication.') 304 parser.add_argument( 305 '--basic-auth-credential', 306 dest='basic_auth_credential', 307 default='test:test', 308 help='Specifies the credential of basic authentication ' 309 'by username:password pair (e.g. test:test).') 310 parser.add_argument('-l', 311 '--log-file', 312 '--log_file', 313 dest='log_file', 314 default='', 315 help='Log file.') 316 # Custom log level: 317 # - FINE: Prints status of each frame processing step 318 parser.add_argument('--log-level', 319 '--log_level', 320 type=six.text_type, 321 dest='log_level', 322 default='warn', 323 choices=[ 324 'fine', 'debug', 'info', 'warning', 'warn', 325 'error', 'critical' 326 ], 327 help='Log level.') 328 parser.add_argument( 329 '--deflate-log-level', 330 '--deflate_log_level', 331 type=six.text_type, 332 dest='deflate_log_level', 333 default='warn', 334 choices=['debug', 'info', 'warning', 'warn', 'error', 'critical'], 335 help='Log level for _Deflater and _Inflater.') 336 parser.add_argument('--thread-monitor-interval-in-sec', 337 '--thread_monitor_interval_in_sec', 338 dest='thread_monitor_interval_in_sec', 339 type=int, 340 default=-1, 341 help=('If positive integer is specified, run a thread ' 342 'monitor to show the status of server threads ' 343 'periodically in the specified inteval in ' 344 'second. If non-positive integer is specified, ' 345 'disable the thread monitor.')) 346 parser.add_argument('--log-max', 347 '--log_max', 348 dest='log_max', 349 type=int, 350 default=_DEFAULT_LOG_MAX_BYTES, 351 help='Log maximum bytes') 352 parser.add_argument('--log-count', 353 '--log_count', 354 dest='log_count', 355 type=int, 356 default=_DEFAULT_LOG_BACKUP_COUNT, 357 help='Log backup count') 358 parser.add_argument('-q', 359 '--queue', 360 dest='request_queue_size', 361 type=int, 362 default=_DEFAULT_REQUEST_QUEUE_SIZE, 363 help='request queue size') 364 parser.add_argument( 365 '--handler-encoding', 366 '--handler_encoding', 367 dest='handler_encoding', 368 type=six.text_type, 369 default=None, 370 help=('Text encoding used for loading handlers. ' 371 'By default, the encoding from the locale is used when ' 372 'reading handler files, but this option can override it. ' 373 'Any encoding supported by the codecs module may be used.')) 374 375 return parser 376 377 378 def _parse_args_and_config(args): 379 parser = _build_option_parser() 380 381 # First, parse options without configuration file. 382 temporary_options, temporary_args = parser.parse_known_args(args=args) 383 if temporary_args: 384 logging.critical('Unrecognized positional arguments: %r', 385 temporary_args) 386 sys.exit(1) 387 388 if temporary_options.config_file: 389 try: 390 config_fp = open(temporary_options.config_file, 'r') 391 except IOError as e: 392 logging.critical('Failed to open configuration file %r: %r', 393 temporary_options.config_file, e) 394 sys.exit(1) 395 396 config_parser = configparser.SafeConfigParser() 397 config_parser.readfp(config_fp) 398 config_fp.close() 399 400 args_from_config = [] 401 for name, value in config_parser.items('pywebsocket'): 402 args_from_config.append('--' + name) 403 args_from_config.append(value) 404 if args is None: 405 args = args_from_config 406 else: 407 args = args_from_config + args 408 return parser.parse_known_args(args=args) 409 else: 410 return temporary_options, temporary_args 411 412 413 def _main(args=None): 414 """You can call this function from your own program, but please note that 415 this function has some side-effects that might affect your program. For 416 example, it changes the current directory. 417 """ 418 419 options, args = _parse_args_and_config(args=args) 420 421 os.chdir(options.document_root) 422 423 server_util.configure_logging(options) 424 425 # TODO(tyoshino): Clean up initialization of CGI related values. Move some 426 # of code here to WebSocketRequestHandler class if it's better. 427 options.cgi_directories = [] 428 options.is_executable_method = None 429 if options.cgi_paths: 430 options.cgi_directories = options.cgi_paths.split(',') 431 if sys.platform in ('cygwin', 'win32'): 432 cygwin_path = None 433 # For Win32 Python, it is expected that CYGWIN_PATH 434 # is set to a directory of cygwin binaries. 435 # For example, websocket_server.py in Chromium sets CYGWIN_PATH to 436 # full path of third_party/cygwin/bin. 437 if 'CYGWIN_PATH' in os.environ: 438 cygwin_path = os.environ['CYGWIN_PATH'] 439 440 def __check_script(scriptpath): 441 return util.get_script_interp(scriptpath, cygwin_path) 442 443 options.is_executable_method = __check_script 444 445 if options.use_tls: 446 logging.debug('Using ssl module') 447 448 if not options.private_key or not options.certificate: 449 logging.critical( 450 'To use TLS, specify private_key and certificate.') 451 sys.exit(1) 452 453 if (options.tls_client_cert_optional and not options.tls_client_auth): 454 logging.critical('Client authentication must be enabled to ' 455 'specify tls_client_cert_optional') 456 sys.exit(1) 457 else: 458 if options.tls_client_auth: 459 logging.critical('TLS must be enabled for client authentication.') 460 sys.exit(1) 461 462 if options.tls_client_cert_optional: 463 logging.critical('TLS must be enabled for client authentication.') 464 sys.exit(1) 465 466 if not options.scan_dir: 467 options.scan_dir = options.websock_handlers 468 469 if options.use_basic_auth: 470 options.basic_auth_credential = 'Basic ' + base64.b64encode( 471 options.basic_auth_credential.encode('UTF-8')).decode() 472 473 try: 474 if options.thread_monitor_interval_in_sec > 0: 475 # Run a thread monitor to show the status of server threads for 476 # debugging. 477 server_util.ThreadMonitor( 478 options.thread_monitor_interval_in_sec).start() 479 480 server = WebSocketServer(options) 481 server.serve_forever() 482 except Exception as e: 483 logging.critical('mod_pywebsocket: %s' % e) 484 logging.critical('mod_pywebsocket: %s' % traceback.format_exc()) 485 sys.exit(1) 486 487 488 if __name__ == '__main__': 489 _main(sys.argv[1:]) 490 491 # vi:sts=4 sw=4 et