#!/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')

    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, util.wrap_popen3_for_win use in this method replaces implementation
    of os.popen3.
    """

    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']
            util.wrap_popen3_for_win(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
