Source code for wsgidav.server.server_cli

# -*- coding: utf-8 -*-
"""
server_cli
==========

:Author: Martin Wendt
:Copyright: Licensed under the MIT license, see LICENSE file in this package.

Standalone server that runs WsgiDAV.

These tasks are performed:

    - Set up the configuration from defaults, configuration file, and command line
      options.
    - Instantiate the WsgiDAVApp object (which is a WSGI application)
    - Start a WSGI server for this WsgiDAVApp object

Configuration is defined like this:

    1. Get the name of a configuration file from command line option
       ``--config-file=FILENAME`` (or short ``-cFILENAME``).
       If this option is omitted, we use ``wsgidav.conf`` in the current
       directory.
    2. Set reasonable default settings.
    3. If configuration file exists: read and use it to overwrite defaults.
    4. If command line options are passed, use them to override settings:

       ``--host`` option overrides ``hostname`` setting.

       ``--port`` option overrides ``port`` setting.

       ``--root=FOLDER`` option creates a FilesystemProvider that publishes
       FOLDER on the '/' share.
"""
from __future__ import print_function

import argparse
import copy
import io
import logging
import os
import platform
import sys
import traceback
from inspect import isfunction
from pprint import pformat
from threading import Timer

import yaml

from wsgidav import __version__, util
from wsgidav.default_conf import DEFAULT_CONFIG, DEFAULT_VERBOSE
from wsgidav.fs_dav_provider import FilesystemProvider
from wsgidav.wsgidav_app import WsgiDAVApp
from wsgidav.xml_tools import use_lxml

try:
    # Try pyjson5 first because it's faster than json5
    from pyjson5 import load as json_load
except ImportError:
    from json5 import load as json_load


__docformat__ = "reStructuredText"

#: Try this config files if no --config=... option is specified
DEFAULT_CONFIG_FILES = ("wsgidav.yaml", "wsgidav.json", "wsgidav.conf")

_logger = logging.getLogger("wsgidav")


def _get_checked_path(path, config, must_exist=True, allow_none=True):
    """Convert path to absolute if not None."""
    if path in (None, ""):
        if allow_none:
            return None
        raise ValueError("Invalid path {!r}".format(path))
    # Evaluate path relative to the folder of the config file (if any)
    config_file = config.get("_config_file")
    if config_file and not os.path.isabs(path):
        path = os.path.normpath(os.path.join(os.path.dirname(config_file), path))
    else:
        path = os.path.abspath(path)
    if must_exist and not os.path.exists(path):
        raise ValueError("Invalid path {!r}".format(path))
    return path


[docs]class FullExpandedPath(argparse.Action): """Expand user- and relative-paths"""
[docs] def __call__(self, parser, namespace, values, option_string=None): new_val = os.path.abspath(os.path.expanduser(values)) setattr(namespace, self.dest, new_val)
def _init_command_line_options(): """Parse command line options into a dictionary.""" description = """\ Run a WEBDAV server to share file system folders. Examples: Share filesystem folder '/temp' for anonymous access (no config file used): wsgidav --port=80 --host=0.0.0.0 --root=/temp --auth=anonymous Run using a specific configuration file: wsgidav --port=80 --host=0.0.0.0 --config=~/my_wsgidav.yaml If no config file is specified, the application will look for a file named 'wsgidav.yaml' in the current directory. See http://wsgidav.readthedocs.io/en/latest/run-configure.html for some explanation of the configuration file format. """ epilog = """\ Licensed under the MIT license. See https://github.com/mar10/wsgidav for additional information. """ parser = argparse.ArgumentParser( prog="wsgidav", description=description, epilog=epilog, # allow_abbrev=False, # Py3.5+ formatter_class=argparse.RawTextHelpFormatter, ) parser.add_argument( "-p", "--port", dest="port", type=int, # default=8080, help="port to serve on (default: 8080)", ) parser.add_argument( "-H", # '-h' conflicts with --help "--host", dest="host", help=( "host to serve from (default: localhost). 'localhost' is only " "accessible from the local computer. Use 0.0.0.0 to make your " "application public" ), ) parser.add_argument( "-r", "--root", dest="root_path", action=FullExpandedPath, help="path to a file system folder to publish as share '/'.", ) parser.add_argument( "--auth", choices=("anonymous", "nt", "pam-login"), help="quick configuration of a domain controller when no config file " "is used", ) parser.add_argument( "--server", choices=SUPPORTED_SERVERS.keys(), # default="cheroot", help="type of pre-installed WSGI server to use (default: cheroot).", ) parser.add_argument( "--ssl-adapter", choices=("builtin", "pyopenssl"), # default="builtin", help="used by 'cheroot' server if SSL certificates are configured " "(default: builtin).", ) qv_group = parser.add_mutually_exclusive_group() qv_group.add_argument( "-v", "--verbose", action="count", default=3, help="increment verbosity by one (default: %(default)s, range: 0..5)", ) qv_group.add_argument( "-q", "--quiet", default=0, action="count", help="decrement verbosity by one" ) qv_group = parser.add_mutually_exclusive_group() qv_group.add_argument( "-c", "--config", dest="config_file", action=FullExpandedPath, help=( "configuration file (default: {} in current directory)".format( DEFAULT_CONFIG_FILES ) ), ) qv_group.add_argument( "--no-config", action="store_true", dest="no_config", help="do not try to load default {}".format(DEFAULT_CONFIG_FILES), ) parser.add_argument( "-V", "--version", action="store_true", help="print version info and exit (may be combined with --verbose)", ) args = parser.parse_args() args.verbose -= args.quiet del args.quiet if args.root_path and not os.path.isdir(args.root_path): msg = "{} is not a directory".format(args.root_path) raise parser.error(msg) if args.version: if args.verbose >= 4: version_info = "WsgiDAV/{} Python/{}({} bit) {}".format( __version__, util.PYTHON_VERSION, "64" if sys.maxsize > 2 ** 32 else "32", platform.platform(aliased=True), ) version_info += "\nPython from: {}".format(sys.executable) else: version_info = "{}".format(__version__) print(version_info) sys.exit() if args.no_config: pass # ... else ignore default config files elif args.config_file is None: # If --config was omitted, use default (if it exists) for filename in DEFAULT_CONFIG_FILES: defPath = os.path.abspath(filename) if os.path.exists(defPath): if args.verbose >= 3: print("Using default configuration file: {}".format(defPath)) args.config_file = defPath break else: # If --config was specified convert to absolute path and assert it exists args.config_file = os.path.abspath(args.config_file) if not os.path.isfile(args.config_file): parser.error( "Could not find specified configuration file: {}".format( args.config_file ) ) # Convert args object to dictionary cmdLineOpts = args.__dict__.copy() if args.verbose >= 5: print("Command line args:") for k, v in cmdLineOpts.items(): print(" {:>12}: {}".format(k, v)) return cmdLineOpts, parser def _read_config_file(config_file, verbose): """Read configuration file options into a dictionary.""" config_file = os.path.abspath(config_file) if not os.path.exists(config_file): raise RuntimeError("Couldn't open configuration file '{}'.".format(config_file)) if config_file.endswith(".json"): with io.open(config_file, mode="r", encoding="utf-8") as json_file: conf = json_load(json_file) elif config_file.endswith(".yaml"): with io.open(config_file, mode="r", encoding="utf-8") as yaml_file: conf = yaml.safe_load(yaml_file) else: try: import imp conf = {} configmodule = imp.load_source("configuration_module", config_file) for k, v in vars(configmodule).items(): if k.startswith("__"): continue elif isfunction(v): continue conf[k] = v except Exception: exc_type, exc_value = sys.exc_info()[:2] exc_info_list = traceback.format_exception_only(exc_type, exc_value) exc_text = "\n".join(exc_info_list) print( "Failed to read configuration file: " + config_file + "\nDue to " + exc_text, file=sys.stderr, ) raise conf["_config_file"] = config_file return conf def _init_config(): """Setup configuration dictionary from default, command line and configuration file.""" cli_opts, parser = _init_command_line_options() cli_verbose = cli_opts["verbose"] # Set config defaults config = copy.deepcopy(DEFAULT_CONFIG) # Configuration file overrides defaults config_file = cli_opts.get("config_file") if config_file: file_opts = _read_config_file(config_file, cli_verbose) util.deep_update(config, file_opts) if cli_verbose != DEFAULT_VERBOSE and "verbose" in file_opts: if cli_verbose >= 2: print( "Config file defines 'verbose: {}' but is overridden by command line: {}.".format( file_opts["verbose"], cli_verbose ) ) config["verbose"] = cli_verbose else: if cli_verbose >= 2: print("Running without configuration file.") # Command line overrides file if cli_opts.get("port"): config["port"] = cli_opts.get("port") if cli_opts.get("host"): config["host"] = cli_opts.get("host") if cli_opts.get("profile") is not None: config["profile"] = True if cli_opts.get("server") is not None: config["server"] = cli_opts.get("server") if cli_opts.get("ssl_adapter") is not None: config["ssl_adapter"] = cli_opts.get("ssl_adapter") # Command line overrides file only if -v or -q where passed: if cli_opts.get("verbose") != DEFAULT_VERBOSE: config["verbose"] = cli_opts.get("verbose") if cli_opts.get("root_path"): root_path = os.path.abspath(cli_opts.get("root_path")) config["provider_mapping"]["/"] = FilesystemProvider(root_path) if config["verbose"] >= 5: # TODO: remove passwords from user_mapping # config_cleaned = copy.deepcopy(config) print("Configuration({}):\n{}".format(cli_opts["config_file"], pformat(config))) if not config["provider_mapping"]: parser.error("No DAV provider defined.") # Quick-configuration of DomainController auth = cli_opts.get("auth") auth_conf = config.get("http_authenticator", {}) if auth and auth_conf.get("domain_controller"): parser.error( "--auth option can only be used when no domain_controller is configured" ) if auth == "anonymous": if config["simple_dc"]["user_mapping"]: parser.error( "--auth=anonymous can only be used when no user_mapping is configured" ) auth_conf.update( { "domain_controller": "wsgidav.dc.simple_dc.SimpleDomainController", "accept_basic": True, "accept_digest": True, "default_to_digest": True, } ) config["simple_dc"]["user_mapping"] = {"*": True} elif auth == "nt": if config.get("nt_dc"): parser.error( "--auth=nt can only be used when no nt_dc settings are configured" ) auth_conf.update( { "domain_controller": "wsgidav.dc.nt_dc.NTDomainController", "accept_basic": True, "accept_digest": False, "default_to_digest": False, } ) config["nt_dc"] = {} elif auth == "pam-login": if config.get("pam_dc"): parser.error( "--auth=pam-login can only be used when no pam_dc settings are configured" ) auth_conf.update( { "domain_controller": "wsgidav.dc.pam_dc.PAMDomainController", "accept_basic": True, "accept_digest": False, "default_to_digest": False, } ) config["pam_dc"] = {"service": "login"} # print(config) if cli_opts.get("reload"): print("Installing paste.reloader.", file=sys.stderr) from paste import reloader # @UnresolvedImport reloader.install() if config_file: # Add config file changes reloader.watch_file(config_file) # import pydevd # pydevd.settrace() return config def _run_paste(app, config, mode): """Run WsgiDAV using paste.httpserver, if Paste is installed. See http://pythonpaste.org/modules/httpserver.html for more options """ from paste import httpserver version = "WsgiDAV/{} {} Python {}".format( __version__, httpserver.WSGIHandler.server_version, util.PYTHON_VERSION ) _logger.info("Running {}...".format(version)) # See http://pythonpaste.org/modules/httpserver.html for more options server = httpserver.serve( app, host=config["host"], port=config["port"], server_version=version, # This option enables handling of keep-alive # and expect-100: protocol_version="HTTP/1.1", start_loop=False, ) if config["verbose"] >= 5: __handle_one_request = server.RequestHandlerClass.handle_one_request def handle_one_request(self): __handle_one_request(self) if self.close_connection == 1: _logger.debug("HTTP Connection : close") else: _logger.debug("HTTP Connection : continue") server.RequestHandlerClass.handle_one_request = handle_one_request # __handle = server.RequestHandlerClass.handle # def handle(self): # _logger.debug("open HTTP connection") # __handle(self) server.RequestHandlerClass.handle_one_request = handle_one_request host, port = server.server_address if host == "0.0.0.0": _logger.info( "Serving on 0.0.0.0:{} view at {}://127.0.0.1:{}".format(port, "http", port) ) else: _logger.info("Serving on {}://{}:{}".format("http", host, port)) try: server.serve_forever() except KeyboardInterrupt: _logger.warning("Caught Ctrl-C, shutting down...") return def _run_gevent(app, config, mode): """Run WsgiDAV using gevent if gevent is installed. See https://github.com/gevent/gevent/blob/master/src/gevent/pywsgi.py#L1356 https://github.com/gevent/gevent/blob/master/src/gevent/server.py#L38 for more options """ import gevent import gevent.monkey gevent.monkey.patch_all() from gevent.pywsgi import WSGIServer server_args = {"bind_addr": (config["host"], config["port"]), "wsgi_app": app} server_name = "WsgiDAV/{} gevent/{} Python/{}".format( __version__, gevent.__version__, util.PYTHON_VERSION ) # Support SSL ssl_certificate = _get_checked_path(config.get("ssl_certificate"), config) ssl_private_key = _get_checked_path(config.get("ssl_private_key"), config) ssl_certificate_chain = _get_checked_path( config.get("ssl_certificate_chain"), config ) # Override or add custom args server_args.update(config.get("server_args", {})) protocol = "http" if ssl_certificate: assert ssl_private_key protocol = "https" _logger.info("SSL / HTTPS enabled.") dav_server = WSGIServer( server_args["bind_addr"], app, keyfile=ssl_private_key, certfile=ssl_certificate, ca_certs=ssl_certificate_chain, ) else: dav_server = WSGIServer(server_args["bind_addr"], app) # If the caller passed a startup event, monkey patch the server to set it # when the request handler loop is entered startup_event = config.get("startup_event") if startup_event: def _patched_start(): dav_server.start_accepting = org_start # undo the monkey patch org_start() _logger.info("gevent is ready") startup_event.set() org_start = dav_server.start_accepting dav_server.start_accepting = _patched_start _logger.info("Running {}".format(server_name)) _logger.info( "Serving on {}://{}:{} ...".format(protocol, config["host"], config["port"]) ) try: gevent.spawn(dav_server.serve_forever()) except KeyboardInterrupt: _logger.warning("Caught Ctrl-C, shutting down...") return def _run__cherrypy(app, config, mode): """Run WsgiDAV using cherrypy.wsgiserver if CherryPy is installed.""" assert mode == "cherrypy-wsgiserver" try: from cherrypy import wsgiserver from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter _logger.warning("WARNING: cherrypy.wsgiserver is deprecated.") _logger.warning( " Starting with CherryPy 9.0 the functionality from cherrypy.wsgiserver" ) _logger.warning(" was moved to the cheroot project.") _logger.warning(" Consider using --server=cheroot.") except ImportError: _logger.error("*" * 78) _logger.error("ERROR: Could not import cherrypy.wsgiserver.") _logger.error( "Try `pip install cherrypy` or specify another server using the --server option." ) _logger.error("Note that starting with CherryPy 9.0, the server was moved to") _logger.error( "the cheroot project, so it is recommended to use `-server=cheroot`" ) _logger.error("and run `pip install cheroot` instead.") _logger.error("*" * 78) raise server_name = "WsgiDAV/{} {} Python/{}".format( __version__, wsgiserver.CherryPyWSGIServer.version, util.PYTHON_VERSION ) wsgiserver.CherryPyWSGIServer.version = server_name # Support SSL ssl_certificate = _get_checked_path(config.get("ssl_certificate"), config) ssl_private_key = _get_checked_path(config.get("ssl_private_key"), config) ssl_certificate_chain = _get_checked_path( config.get("ssl_certificate_chain"), config ) protocol = "http" if ssl_certificate: assert ssl_private_key wsgiserver.CherryPyWSGIServer.ssl_adapter = BuiltinSSLAdapter( ssl_certificate, ssl_private_key, ssl_certificate_chain ) protocol = "https" _logger.info("SSL / HTTPS enabled.") _logger.info("Running {}".format(server_name)) _logger.info( "Serving on {}://{}:{} ...".format(protocol, config["host"], config["port"]) ) server_args = { "bind_addr": (config["host"], config["port"]), "wsgi_app": app, "server_name": server_name, } # Override or add custom args server_args.update(config.get("server_args", {})) server = wsgiserver.CherryPyWSGIServer(**server_args) # If the caller passed a startup event, monkey patch the server to set it # when the request handler loop is entered startup_event = config.get("startup_event") if startup_event: def _patched_tick(): server.tick = org_tick # undo the monkey patch org_tick() _logger.info("CherryPyWSGIServer is ready") startup_event.set() org_tick = server.tick server.tick = _patched_tick try: server.start() except KeyboardInterrupt: _logger.warning("Caught Ctrl-C, shutting down...") finally: server.stop() return def _run_cheroot(app, config, mode): """Run WsgiDAV using cheroot.server if Cheroot is installed.""" assert mode == "cheroot" try: from cheroot import server, wsgi except ImportError: _logger.error("*" * 78) _logger.error("ERROR: Could not import Cheroot.") _logger.error( "Try `pip install cheroot` or specify another server using the --server option." ) _logger.error("*" * 78) raise server_name = "WsgiDAV/{} {} Python/{}".format( __version__, wsgi.Server.version, util.PYTHON_VERSION ) wsgi.Server.version = server_name # Support SSL ssl_certificate = _get_checked_path(config.get("ssl_certificate"), config) ssl_private_key = _get_checked_path(config.get("ssl_private_key"), config) ssl_certificate_chain = _get_checked_path( config.get("ssl_certificate_chain"), config ) ssl_adapter = config.get("ssl_adapter", "builtin") protocol = "http" if ssl_certificate and ssl_private_key: ssl_adapter = server.get_ssl_adapter_class(ssl_adapter) wsgi.Server.ssl_adapter = ssl_adapter( ssl_certificate, ssl_private_key, ssl_certificate_chain ) protocol = "https" _logger.info("SSL / HTTPS enabled. Adapter: {}".format(ssl_adapter)) elif ssl_certificate or ssl_private_key: raise RuntimeError( "Option 'ssl_certificate' and 'ssl_private_key' must be used together." ) _logger.info("Running {}".format(server_name)) _logger.info( "Serving on {}://{}:{} ...".format(protocol, config["host"], config["port"]) ) server_args = { "bind_addr": (config["host"], config["port"]), "wsgi_app": app, "server_name": server_name, # File Explorer needs lot of threads (see issue #149): "numthreads": 50, } # Override or add custom args server_args.update(config.get("server_args", {})) class PatchedServer(wsgi.Server): STARTUP_NOTIFICATION_DELAY = 0.5 def serve(self, *args, **kwargs): _logger.error("wsgi.Server.serve") if startup_event and not startup_event.is_set(): Timer(self.STARTUP_NOTIFICATION_DELAY, startup_event.set).start() _logger.error("wsgi.Server is ready") return super(PatchedServer, self).serve(*args, **kwargs) # If the caller passed a startup event, monkey patch the server to set it # when the request handler loop is entered startup_event = config.get("startup_event") if startup_event: server = PatchedServer(**server_args) # issue #200: The `server.tick()` method was dropped with cheroot 8.5 # def _patched_tick(): # server.tick = org_tick # undo the monkey patch # _logger.info("wsgi.Server is ready (pre Cheroot 8.5") # startup_event.set() # org_tick() # # org_tick = server.tick # server.tick = _patched_tick else: server = wsgi.Server(**server_args) try: server.start() except KeyboardInterrupt: _logger.warning("Caught Ctrl-C, shutting down...") finally: server.stop() return def _run_flup(app, config, mode): """Run WsgiDAV using flup.server.fcgi if Flup is installed.""" # http://trac.saddi.com/flup/wiki/FlupServers if mode == "flup-fcgi": from flup.server.fcgi import WSGIServer from flup.server.fcgi import __version__ as flupver elif mode == "flup-fcgi-fork": from flup.server.fcgi_fork import WSGIServer from flup.server.fcgi_fork import __version__ as flupver else: raise ValueError _logger.info( "Running WsgiDAV/{} {}/{}...".format( __version__, WSGIServer.__module__, flupver ) ) server = WSGIServer( app, bindAddress=(config["host"], config["port"]), # debug=True, ) try: server.run() except KeyboardInterrupt: _logger.warning("Caught Ctrl-C, shutting down...") return def _run_wsgiref(app, config, mode): """Run WsgiDAV using wsgiref.simple_server, on Python 2.5+.""" # http://www.python.org/doc/2.5.2/lib/module-wsgiref.html from wsgiref.simple_server import make_server, software_version version = "WsgiDAV/{} {}".format(__version__, software_version) _logger.info("Running {}...".format(version)) _logger.warning( "WARNING: This single threaded server (wsgiref) is not meant for production." ) httpd = make_server(config["host"], config["port"], app) try: httpd.serve_forever() except KeyboardInterrupt: _logger.warning("Caught Ctrl-C, shutting down...") return def _run_ext_wsgiutils(app, config, mode): """Run WsgiDAV using ext_wsgiutils_server from the wsgidav package.""" from wsgidav.server import ext_wsgiutils_server _logger.info( "Running WsgiDAV {} on wsgidav.ext_wsgiutils_server...".format(__version__) ) _logger.warning( "WARNING: This single threaded server (ext-wsgiutils) is not meant for production." ) try: ext_wsgiutils_server.serve(config, app) except KeyboardInterrupt: _logger.warning("Caught Ctrl-C, shutting down...") return def _run_gunicorn(app, config, mode): """Run WsgiDAV using gunicorn if gunicorn is installed.""" import gunicorn.app.base class GunicornApplication(gunicorn.app.base.BaseApplication): def __init__(self, app, options=None): self.options = options or {} self.application = app super().__init__() def load_config(self): config = { key: value for key, value in self.options.items() if key in self.cfg.settings and value is not None } for key, value in config.items(): self.cfg.set(key.lower(), value) def load(self): return self.application options = { "bind": "{}:{}".format(config["host"], config["port"]), "threads": 50, "timeout": 1200, } GunicornApplication(app, options).run() SUPPORTED_SERVERS = { "paste": _run_paste, "gevent": _run_gevent, "cheroot": _run_cheroot, "cherrypy": _run__cherrypy, "ext-wsgiutils": _run_ext_wsgiutils, "flup-fcgi": _run_flup, "flup-fcgi_fork": _run_flup, "wsgiref": _run_wsgiref, "gunicorn": _run_gunicorn, }
[docs]def run(): config = _init_config() util.init_logging(config) app = WsgiDAVApp(config) server = config["server"] handler = SUPPORTED_SERVERS.get(server) if not handler: raise RuntimeError( "Unsupported server type {!r} (expected {!r})".format( server, "', '".join(SUPPORTED_SERVERS.keys()) ) ) if not use_lxml and config["verbose"] >= 3: _logger.warning( "Could not import lxml: using xml instead (up to 10% slower). " "Consider `pip install lxml`(see https://pypi.python.org/pypi/lxml)." ) handler(app, config, server)
if __name__ == "__main__": # Just in case... from multiprocessing import freeze_support freeze_support() run()