summaryrefslogtreecommitdiffstats
path: root/testing/mochitest/pywebsocket3/mod_pywebsocket/standalone.py
blob: bd32158790f6c077e85f1e4925b620d5c03d8a6b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
#!/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