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