491 lines
18 KiB
Python
Executable file
491 lines
18 KiB
Python
Executable file
#!/usr/bin/env python
|
|
#
|
|
# Copyright 2012, Google Inc.
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are
|
|
# met:
|
|
#
|
|
# * Redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer.
|
|
# * Redistributions in binary form must reproduce the above
|
|
# copyright notice, this list of conditions and the following disclaimer
|
|
# in the documentation and/or other materials provided with the
|
|
# distribution.
|
|
# * Neither the name of Google Inc. nor the names of its
|
|
# contributors may be used to endorse or promote products derived from
|
|
# this software without specific prior written permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
"""Standalone WebSocket server.
|
|
|
|
Use this file to launch pywebsocket as a standalone server.
|
|
|
|
|
|
BASIC USAGE
|
|
===========
|
|
|
|
Go to the src directory and run
|
|
|
|
$ python mod_pywebsocket/standalone.py [-p <ws_port>]
|
|
[-w <websock_handlers>]
|
|
[-d <document_root>]
|
|
|
|
<ws_port> is the port number to use for ws:// connection.
|
|
|
|
<document_root> is the path to the root directory of HTML files.
|
|
|
|
<websock_handlers> is the path to the root directory of WebSocket handlers.
|
|
If not specified, <document_root> will be used. See __init__.py (or
|
|
run $ pydoc mod_pywebsocket) for how to write WebSocket handlers.
|
|
|
|
For more detail and other options, run
|
|
|
|
$ python mod_pywebsocket/standalone.py --help
|
|
|
|
or see _build_option_parser method below.
|
|
|
|
For trouble shooting, adding "--log_level debug" might help you.
|
|
|
|
|
|
TRY DEMO
|
|
========
|
|
|
|
Go to the src directory and run standalone.py with -d option to set the
|
|
document root to the directory containing example HTMLs and handlers like this:
|
|
|
|
$ cd src
|
|
$ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example
|
|
|
|
to launch pywebsocket with the sample handler and html on port 80. Open
|
|
http://localhost/console.html, click the connect button, type something into
|
|
the text box next to the send button and click the send button. If everything
|
|
is working, you'll see the message you typed echoed by the server.
|
|
|
|
|
|
USING TLS
|
|
=========
|
|
|
|
To run the standalone server with TLS support, run it with -t, -k, and -c
|
|
options. When TLS is enabled, the standalone server accepts only TLS connection.
|
|
|
|
Note that when ssl module is used and the key/cert location is incorrect,
|
|
TLS connection silently fails while pyOpenSSL fails on startup.
|
|
|
|
Example:
|
|
|
|
$ PYTHONPATH=. python mod_pywebsocket/standalone.py \
|
|
-d example \
|
|
-p 10443 \
|
|
-t \
|
|
-c ../test/cert/cert.pem \
|
|
-k ../test/cert/key.pem \
|
|
|
|
Note that when passing a relative path to -c and -k option, it will be resolved
|
|
using the document root directory as the base.
|
|
|
|
|
|
USING CLIENT AUTHENTICATION
|
|
===========================
|
|
|
|
To run the standalone server with TLS client authentication support, run it with
|
|
--tls-client-auth and --tls-client-ca options in addition to ones required for
|
|
TLS support.
|
|
|
|
Example:
|
|
|
|
$ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example -p 10443 -t \
|
|
-c ../test/cert/cert.pem -k ../test/cert/key.pem \
|
|
--tls-client-auth \
|
|
--tls-client-ca=../test/cert/cacert.pem
|
|
|
|
Note that when passing a relative path to --tls-client-ca option, it will be
|
|
resolved using the document root directory as the base.
|
|
|
|
|
|
CONFIGURATION FILE
|
|
==================
|
|
|
|
You can also write a configuration file and use it by specifying the path to
|
|
the configuration file by --config option. Please write a configuration file
|
|
following the documentation of the Python ConfigParser library. Name of each
|
|
entry must be the long version argument name. E.g. to set log level to debug,
|
|
add the following line:
|
|
|
|
log_level=debug
|
|
|
|
For options which doesn't take value, please add some fake value. E.g. for
|
|
--tls option, add the following line:
|
|
|
|
tls=True
|
|
|
|
Note that tls will be enabled even if you write tls=False as the value part is
|
|
fake.
|
|
|
|
When both a command line argument and a configuration file entry are set for
|
|
the same configuration item, the command line value will override one in the
|
|
configuration file.
|
|
|
|
|
|
THREADING
|
|
=========
|
|
|
|
This server is derived from SocketServer.ThreadingMixIn. Hence a thread is
|
|
used for each request.
|
|
|
|
|
|
SECURITY WARNING
|
|
================
|
|
|
|
This uses CGIHTTPServer and CGIHTTPServer is not secure.
|
|
It may execute arbitrary Python code or external programs. It should not be
|
|
used outside a firewall.
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
from six.moves import configparser
|
|
import base64
|
|
import logging
|
|
import argparse
|
|
import os
|
|
import six
|
|
import sys
|
|
import traceback
|
|
|
|
from mod_pywebsocket import common
|
|
from mod_pywebsocket import util
|
|
from mod_pywebsocket import server_util
|
|
from mod_pywebsocket.websocket_server import WebSocketServer
|
|
|
|
_DEFAULT_LOG_MAX_BYTES = 1024 * 256
|
|
_DEFAULT_LOG_BACKUP_COUNT = 5
|
|
|
|
_DEFAULT_REQUEST_QUEUE_SIZE = 128
|
|
|
|
|
|
def _build_option_parser():
|
|
parser = argparse.ArgumentParser()
|
|
|
|
parser.add_argument(
|
|
'--config',
|
|
dest='config_file',
|
|
type=six.text_type,
|
|
default=None,
|
|
help=('Path to configuration file. See the file comment '
|
|
'at the top of this file for the configuration '
|
|
'file format'))
|
|
parser.add_argument('-H',
|
|
'--server-host',
|
|
'--server_host',
|
|
dest='server_host',
|
|
default='',
|
|
help='server hostname to listen to')
|
|
parser.add_argument('-V',
|
|
'--validation-host',
|
|
'--validation_host',
|
|
dest='validation_host',
|
|
default=None,
|
|
help='server hostname to validate in absolute path.')
|
|
parser.add_argument('-p',
|
|
'--port',
|
|
dest='port',
|
|
type=int,
|
|
default=common.DEFAULT_WEB_SOCKET_PORT,
|
|
help='port to listen to')
|
|
parser.add_argument('-P',
|
|
'--validation-port',
|
|
'--validation_port',
|
|
dest='validation_port',
|
|
type=int,
|
|
default=None,
|
|
help='server port to validate in absolute path.')
|
|
parser.add_argument(
|
|
'-w',
|
|
'--websock-handlers',
|
|
'--websock_handlers',
|
|
dest='websock_handlers',
|
|
default='.',
|
|
help=('The root directory of WebSocket handler files. '
|
|
'If the path is relative, --document-root is used '
|
|
'as the base.'))
|
|
parser.add_argument('-m',
|
|
'--websock-handlers-map-file',
|
|
'--websock_handlers_map_file',
|
|
dest='websock_handlers_map_file',
|
|
default=None,
|
|
help=('WebSocket handlers map file. '
|
|
'Each line consists of alias_resource_path and '
|
|
'existing_resource_path, separated by spaces.'))
|
|
parser.add_argument('-s',
|
|
'--scan-dir',
|
|
'--scan_dir',
|
|
dest='scan_dir',
|
|
default=None,
|
|
help=('Must be a directory under --websock-handlers. '
|
|
'Only handlers under this directory are scanned '
|
|
'and registered to the server. '
|
|
'Useful for saving scan time when the handler '
|
|
'root directory contains lots of files that are '
|
|
'not handler file or are handler files but you '
|
|
'don\'t want them to be registered. '))
|
|
parser.add_argument(
|
|
'--allow-handlers-outside-root-dir',
|
|
'--allow_handlers_outside_root_dir',
|
|
dest='allow_handlers_outside_root_dir',
|
|
action='store_true',
|
|
default=False,
|
|
help=('Scans WebSocket handlers even if their canonical '
|
|
'path is not under --websock-handlers.'))
|
|
parser.add_argument('-d',
|
|
'--document-root',
|
|
'--document_root',
|
|
dest='document_root',
|
|
default='.',
|
|
help='Document root directory.')
|
|
parser.add_argument('-x',
|
|
'--cgi-paths',
|
|
'--cgi_paths',
|
|
dest='cgi_paths',
|
|
default=None,
|
|
help=('CGI paths relative to document_root.'
|
|
'Comma-separated. (e.g -x /cgi,/htbin) '
|
|
'Files under document_root/cgi_path are handled '
|
|
'as CGI programs. Must be executable.'))
|
|
parser.add_argument('-t',
|
|
'--tls',
|
|
dest='use_tls',
|
|
action='store_true',
|
|
default=False,
|
|
help='use TLS (wss://)')
|
|
parser.add_argument('-k',
|
|
'--private-key',
|
|
'--private_key',
|
|
dest='private_key',
|
|
default='',
|
|
help='TLS private key file.')
|
|
parser.add_argument('-c',
|
|
'--certificate',
|
|
dest='certificate',
|
|
default='',
|
|
help='TLS certificate file.')
|
|
parser.add_argument('--tls-client-auth',
|
|
dest='tls_client_auth',
|
|
action='store_true',
|
|
default=False,
|
|
help='Requests TLS client auth on every connection.')
|
|
parser.add_argument('--tls-client-cert-optional',
|
|
dest='tls_client_cert_optional',
|
|
action='store_true',
|
|
default=False,
|
|
help=('Makes client certificate optional even though '
|
|
'TLS client auth is enabled.'))
|
|
parser.add_argument('--tls-client-ca',
|
|
dest='tls_client_ca',
|
|
default='',
|
|
help=('Specifies a pem file which contains a set of '
|
|
'concatenated CA certificates which are used to '
|
|
'validate certificates passed from clients'))
|
|
parser.add_argument('--basic-auth',
|
|
dest='use_basic_auth',
|
|
action='store_true',
|
|
default=False,
|
|
help='Requires Basic authentication.')
|
|
parser.add_argument(
|
|
'--basic-auth-credential',
|
|
dest='basic_auth_credential',
|
|
default='test:test',
|
|
help='Specifies the credential of basic authentication '
|
|
'by username:password pair (e.g. test:test).')
|
|
parser.add_argument('-l',
|
|
'--log-file',
|
|
'--log_file',
|
|
dest='log_file',
|
|
default='',
|
|
help='Log file.')
|
|
# Custom log level:
|
|
# - FINE: Prints status of each frame processing step
|
|
parser.add_argument('--log-level',
|
|
'--log_level',
|
|
type=six.text_type,
|
|
dest='log_level',
|
|
default='warn',
|
|
choices=[
|
|
'fine', 'debug', 'info', 'warning', 'warn',
|
|
'error', 'critical'
|
|
],
|
|
help='Log level.')
|
|
parser.add_argument(
|
|
'--deflate-log-level',
|
|
'--deflate_log_level',
|
|
type=six.text_type,
|
|
dest='deflate_log_level',
|
|
default='warn',
|
|
choices=['debug', 'info', 'warning', 'warn', 'error', 'critical'],
|
|
help='Log level for _Deflater and _Inflater.')
|
|
parser.add_argument('--thread-monitor-interval-in-sec',
|
|
'--thread_monitor_interval_in_sec',
|
|
dest='thread_monitor_interval_in_sec',
|
|
type=int,
|
|
default=-1,
|
|
help=('If positive integer is specified, run a thread '
|
|
'monitor to show the status of server threads '
|
|
'periodically in the specified inteval in '
|
|
'second. If non-positive integer is specified, '
|
|
'disable the thread monitor.'))
|
|
parser.add_argument('--log-max',
|
|
'--log_max',
|
|
dest='log_max',
|
|
type=int,
|
|
default=_DEFAULT_LOG_MAX_BYTES,
|
|
help='Log maximum bytes')
|
|
parser.add_argument('--log-count',
|
|
'--log_count',
|
|
dest='log_count',
|
|
type=int,
|
|
default=_DEFAULT_LOG_BACKUP_COUNT,
|
|
help='Log backup count')
|
|
parser.add_argument('-q',
|
|
'--queue',
|
|
dest='request_queue_size',
|
|
type=int,
|
|
default=_DEFAULT_REQUEST_QUEUE_SIZE,
|
|
help='request queue size')
|
|
parser.add_argument(
|
|
'--handler-encoding',
|
|
'--handler_encoding',
|
|
dest='handler_encoding',
|
|
type=six.text_type,
|
|
default=None,
|
|
help=('Text encoding used for loading handlers. '
|
|
'By default, the encoding from the locale is used when '
|
|
'reading handler files, but this option can override it. '
|
|
'Any encoding supported by the codecs module may be used.'))
|
|
|
|
return parser
|
|
|
|
|
|
def _parse_args_and_config(args):
|
|
parser = _build_option_parser()
|
|
|
|
# First, parse options without configuration file.
|
|
temporary_options, temporary_args = parser.parse_known_args(args=args)
|
|
if temporary_args:
|
|
logging.critical('Unrecognized positional arguments: %r',
|
|
temporary_args)
|
|
sys.exit(1)
|
|
|
|
if temporary_options.config_file:
|
|
try:
|
|
config_fp = open(temporary_options.config_file, 'r')
|
|
except IOError as e:
|
|
logging.critical('Failed to open configuration file %r: %r',
|
|
temporary_options.config_file, e)
|
|
sys.exit(1)
|
|
|
|
config_parser = configparser.SafeConfigParser()
|
|
config_parser.readfp(config_fp)
|
|
config_fp.close()
|
|
|
|
args_from_config = []
|
|
for name, value in config_parser.items('pywebsocket'):
|
|
args_from_config.append('--' + name)
|
|
args_from_config.append(value)
|
|
if args is None:
|
|
args = args_from_config
|
|
else:
|
|
args = args_from_config + args
|
|
return parser.parse_known_args(args=args)
|
|
else:
|
|
return temporary_options, temporary_args
|
|
|
|
|
|
def _main(args=None):
|
|
"""You can call this function from your own program, but please note that
|
|
this function has some side-effects that might affect your program. For
|
|
example, it changes the current directory.
|
|
"""
|
|
|
|
options, args = _parse_args_and_config(args=args)
|
|
|
|
os.chdir(options.document_root)
|
|
|
|
server_util.configure_logging(options)
|
|
|
|
# TODO(tyoshino): Clean up initialization of CGI related values. Move some
|
|
# of code here to WebSocketRequestHandler class if it's better.
|
|
options.cgi_directories = []
|
|
options.is_executable_method = None
|
|
if options.cgi_paths:
|
|
options.cgi_directories = options.cgi_paths.split(',')
|
|
if sys.platform in ('cygwin', 'win32'):
|
|
cygwin_path = None
|
|
# For Win32 Python, it is expected that CYGWIN_PATH
|
|
# is set to a directory of cygwin binaries.
|
|
# For example, websocket_server.py in Chromium sets CYGWIN_PATH to
|
|
# full path of third_party/cygwin/bin.
|
|
if 'CYGWIN_PATH' in os.environ:
|
|
cygwin_path = os.environ['CYGWIN_PATH']
|
|
|
|
def __check_script(scriptpath):
|
|
return util.get_script_interp(scriptpath, cygwin_path)
|
|
|
|
options.is_executable_method = __check_script
|
|
|
|
if options.use_tls:
|
|
logging.debug('Using ssl module')
|
|
|
|
if not options.private_key or not options.certificate:
|
|
logging.critical(
|
|
'To use TLS, specify private_key and certificate.')
|
|
sys.exit(1)
|
|
|
|
if (options.tls_client_cert_optional and not options.tls_client_auth):
|
|
logging.critical('Client authentication must be enabled to '
|
|
'specify tls_client_cert_optional')
|
|
sys.exit(1)
|
|
else:
|
|
if options.tls_client_auth:
|
|
logging.critical('TLS must be enabled for client authentication.')
|
|
sys.exit(1)
|
|
|
|
if options.tls_client_cert_optional:
|
|
logging.critical('TLS must be enabled for client authentication.')
|
|
sys.exit(1)
|
|
|
|
if not options.scan_dir:
|
|
options.scan_dir = options.websock_handlers
|
|
|
|
if options.use_basic_auth:
|
|
options.basic_auth_credential = 'Basic ' + base64.b64encode(
|
|
options.basic_auth_credential.encode('UTF-8')).decode()
|
|
|
|
try:
|
|
if options.thread_monitor_interval_in_sec > 0:
|
|
# Run a thread monitor to show the status of server threads for
|
|
# debugging.
|
|
server_util.ThreadMonitor(
|
|
options.thread_monitor_interval_in_sec).start()
|
|
|
|
server = WebSocketServer(options)
|
|
server.serve_forever()
|
|
except Exception as e:
|
|
logging.critical('mod_pywebsocket: %s' % e)
|
|
logging.critical('mod_pywebsocket: %s' % traceback.format_exc())
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
_main(sys.argv[1:])
|
|
|
|
# vi:sts=4 sw=4 et
|