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 33Use this file to launch pywebsocket as a standalone server. 34 35 36BASIC USAGE 37=========== 38 39Go 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. 50If not specified, <document_root> will be used. See __init__.py (or 51run $ pydoc mod_pywebsocket) for how to write WebSocket handlers. 52 53For more detail and other options, run 54 55 $ python mod_pywebsocket/standalone.py --help 56 57or see _build_option_parser method below. 58 59For trouble shooting, adding "--log_level debug" might help you. 60 61 62TRY DEMO 63======== 64 65Go to the src directory and run standalone.py with -d option to set the 66document 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 71to launch pywebsocket with the sample handler and html on port 80. Open 72http://localhost/console.html, click the connect button, type something into 73the text box next to the send button and click the send button. If everything 74is working, you'll see the message you typed echoed by the server. 75 76 77USING TLS 78========= 79 80To run the standalone server with TLS support, run it with -t, -k, and -c 81options. When TLS is enabled, the standalone server accepts only TLS connection. 82 83Note that when ssl module is used and the key/cert location is incorrect, 84TLS connection silently fails while pyOpenSSL fails on startup. 85 86Example: 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 95Note that when passing a relative path to -c and -k option, it will be resolved 96using the document root directory as the base. 97 98 99USING CLIENT AUTHENTICATION 100=========================== 101 102To 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 104TLS support. 105 106Example: 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 113Note that when passing a relative path to --tls-client-ca option, it will be 114resolved using the document root directory as the base. 115 116 117CONFIGURATION FILE 118================== 119 120You can also write a configuration file and use it by specifying the path to 121the configuration file by --config option. Please write a configuration file 122following the documentation of the Python ConfigParser library. Name of each 123entry must be the long version argument name. E.g. to set log level to debug, 124add the following line: 125 126log_level=debug 127 128For options which doesn't take value, please add some fake value. E.g. for 129--tls option, add the following line: 130 131tls=True 132 133Note that tls will be enabled even if you write tls=False as the value part is 134fake. 135 136When both a command line argument and a configuration file entry are set for 137the same configuration item, the command line value will override one in the 138configuration file. 139 140 141THREADING 142========= 143 144This server is derived from SocketServer.ThreadingMixIn. Hence a thread is 145used for each request. 146 147 148SECURITY WARNING 149================ 150 151This uses CGIHTTPServer and CGIHTTPServer is not secure. 152It may execute arbitrary Python code or external programs. It should not be 153used outside a firewall. 154""" 155 156from __future__ import absolute_import 157from six.moves import configparser 158import base64 159import logging 160import argparse 161import os 162import six 163import sys 164import traceback 165 166from mod_pywebsocket import common 167from mod_pywebsocket import util 168from mod_pywebsocket import server_util 169from 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 177def _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 365 return parser 366 367 368def _parse_args_and_config(args): 369 parser = _build_option_parser() 370 371 # First, parse options without configuration file. 372 temporary_options, temporary_args = parser.parse_known_args(args=args) 373 if temporary_args: 374 logging.critical('Unrecognized positional arguments: %r', 375 temporary_args) 376 sys.exit(1) 377 378 if temporary_options.config_file: 379 try: 380 config_fp = open(temporary_options.config_file, 'r') 381 except IOError as e: 382 logging.critical('Failed to open configuration file %r: %r', 383 temporary_options.config_file, e) 384 sys.exit(1) 385 386 config_parser = configparser.SafeConfigParser() 387 config_parser.readfp(config_fp) 388 config_fp.close() 389 390 args_from_config = [] 391 for name, value in config_parser.items('pywebsocket'): 392 args_from_config.append('--' + name) 393 args_from_config.append(value) 394 if args is None: 395 args = args_from_config 396 else: 397 args = args_from_config + args 398 return parser.parse_known_args(args=args) 399 else: 400 return temporary_options, temporary_args 401 402 403def _main(args=None): 404 """You can call this function from your own program, but please note that 405 this function has some side-effects that might affect your program. For 406 example, util.wrap_popen3_for_win use in this method replaces implementation 407 of os.popen3. 408 """ 409 410 options, args = _parse_args_and_config(args=args) 411 412 os.chdir(options.document_root) 413 414 server_util.configure_logging(options) 415 416 # TODO(tyoshino): Clean up initialization of CGI related values. Move some 417 # of code here to WebSocketRequestHandler class if it's better. 418 options.cgi_directories = [] 419 options.is_executable_method = None 420 if options.cgi_paths: 421 options.cgi_directories = options.cgi_paths.split(',') 422 if sys.platform in ('cygwin', 'win32'): 423 cygwin_path = None 424 # For Win32 Python, it is expected that CYGWIN_PATH 425 # is set to a directory of cygwin binaries. 426 # For example, websocket_server.py in Chromium sets CYGWIN_PATH to 427 # full path of third_party/cygwin/bin. 428 if 'CYGWIN_PATH' in os.environ: 429 cygwin_path = os.environ['CYGWIN_PATH'] 430 util.wrap_popen3_for_win(cygwin_path) 431 432 def __check_script(scriptpath): 433 return util.get_script_interp(scriptpath, cygwin_path) 434 435 options.is_executable_method = __check_script 436 437 if options.use_tls: 438 logging.debug('Using ssl module') 439 440 if not options.private_key or not options.certificate: 441 logging.critical( 442 'To use TLS, specify private_key and certificate.') 443 sys.exit(1) 444 445 if (options.tls_client_cert_optional and not options.tls_client_auth): 446 logging.critical('Client authentication must be enabled to ' 447 'specify tls_client_cert_optional') 448 sys.exit(1) 449 else: 450 if options.tls_client_auth: 451 logging.critical('TLS must be enabled for client authentication.') 452 sys.exit(1) 453 454 if options.tls_client_cert_optional: 455 logging.critical('TLS must be enabled for client authentication.') 456 sys.exit(1) 457 458 if not options.scan_dir: 459 options.scan_dir = options.websock_handlers 460 461 if options.use_basic_auth: 462 options.basic_auth_credential = 'Basic ' + base64.b64encode( 463 options.basic_auth_credential.encode('UTF-8')).decode() 464 465 try: 466 if options.thread_monitor_interval_in_sec > 0: 467 # Run a thread monitor to show the status of server threads for 468 # debugging. 469 server_util.ThreadMonitor( 470 options.thread_monitor_interval_in_sec).start() 471 472 server = WebSocketServer(options) 473 server.serve_forever() 474 except Exception as e: 475 logging.critical('mod_pywebsocket: %s' % e) 476 logging.critical('mod_pywebsocket: %s' % traceback.format_exc()) 477 sys.exit(1) 478 479 480if __name__ == '__main__': 481 _main(sys.argv[1:]) 482 483# vi:sts=4 sw=4 et 484