wsgidav.server.server_cli.py#

  1# -*- coding: utf-8 -*-
  2"""
  3server_cli
  4==========
  5
  6:Author: Martin Wendt
  7:Copyright: Licensed under the MIT license, see LICENSE file in this package.
  8
  9Standalone server that runs WsgiDAV.
 10
 11These tasks are performed:
 12
 13    - Set up the configuration from defaults, configuration file, and command line
 14      options.
 15    - Instantiate the WsgiDAVApp object (which is a WSGI application)
 16    - Start a WSGI server for this WsgiDAVApp object
 17
 18Configuration is defined like this:
 19
 20    1. Get the name of a configuration file from command line option
 21       ``--config-file=FILENAME`` (or short ``-cFILENAME``).
 22       If this option is omitted, we use ``wsgidav.yaml`` in the current
 23       directory.
 24    2. Set reasonable default settings.
 25    3. If configuration file exists: read and use it to overwrite defaults.
 26    4. If command line options are passed, use them to override settings:
 27
 28       ``--host`` option overrides ``hostname`` setting.
 29
 30       ``--port`` option overrides ``port`` setting.
 31
 32       ``--root=FOLDER`` option creates a FilesystemProvider that publishes
 33       FOLDER on the '/' share.
 34"""
 35import argparse
 36import copy
 37import logging
 38import os
 39import platform
 40import sys
 41import webbrowser
 42from pprint import pformat
 43from threading import Timer
 44
 45import yaml
 46
 47from wsgidav import __version__, util
 48from wsgidav.default_conf import DEFAULT_CONFIG, DEFAULT_VERBOSE
 49from wsgidav.fs_dav_provider import FilesystemProvider
 50from wsgidav.wsgidav_app import WsgiDAVApp
 51from wsgidav.xml_tools import use_lxml
 52
 53try:
 54    # Try pyjson5 first because it's faster than json5
 55    from pyjson5 import load as json_load
 56except ImportError:
 57    from json5 import load as json_load
 58
 59
 60__docformat__ = "reStructuredText"
 61
 62#: Try this config files if no --config=... option is specified
 63DEFAULT_CONFIG_FILES = ("wsgidav.yaml", "wsgidav.json")
 64
 65_logger = logging.getLogger("wsgidav")
 66
 67
 68def _get_common_info(config):
 69    """Calculate some common info."""
 70    # Support SSL
 71    ssl_certificate = util.fix_path(config.get("ssl_certificate"), config)
 72    ssl_private_key = util.fix_path(config.get("ssl_private_key"), config)
 73    ssl_certificate_chain = util.fix_path(config.get("ssl_certificate_chain"), config)
 74    ssl_adapter = config.get("ssl_adapter", "builtin")
 75    use_ssl = False
 76    if ssl_certificate and ssl_private_key:
 77        use_ssl = True
 78        # _logger.info("SSL / HTTPS enabled. Adapter: {}".format(ssl_adapter))
 79    elif ssl_certificate or ssl_private_key:
 80        raise RuntimeError(
 81            "Option 'ssl_certificate' and 'ssl_private_key' must be used together."
 82        )
 83
 84    protocol = "https" if use_ssl else "http"
 85    url = f"{protocol}://{config['host']}:{config['port']}"
 86    info = {
 87        "use_ssl": use_ssl,
 88        "ssl_cert": ssl_certificate,
 89        "ssl_pk": ssl_private_key,
 90        "ssl_adapter": ssl_adapter,
 91        "ssl_chain": ssl_certificate_chain,
 92        "protocol": protocol,
 93        "url": url,
 94    }
 95    return info
 96
 97
 98class FullExpandedPath(argparse.Action):
 99    """Expand user- and relative-paths"""
100
101    def __call__(self, parser, namespace, values, option_string=None):
102        new_val = os.path.abspath(os.path.expanduser(values))
103        setattr(namespace, self.dest, new_val)
104
105
106def _init_command_line_options():
107    """Parse command line options into a dictionary."""
108    description = """\
109
110Run a WEBDAV server to share file system folders.
111
112Examples:
113
114  Share filesystem folder '/temp' for anonymous access (no config file used):
115    wsgidav --port=80 --host=0.0.0.0 --root=/temp --auth=anonymous
116
117  Run using a specific configuration file:
118    wsgidav --port=80 --host=0.0.0.0 --config=~/my_wsgidav.yaml
119
120  If no config file is specified, the application will look for a file named
121  'wsgidav.yaml' in the current directory.
122  See
123    https://wsgidav.readthedocs.io/en/latest/user_guide_configure.html
124  for some explanation of the configuration file format.
125  """
126
127    epilog = """\
128Licensed under the MIT license.
129See https://github.com/mar10/wsgidav for additional information.
130
131"""
132
133    parser = argparse.ArgumentParser(
134        prog="wsgidav",
135        description=description,
136        epilog=epilog,
137        allow_abbrev=False,
138        formatter_class=argparse.RawTextHelpFormatter,
139    )
140    parser.add_argument(
141        "-p",
142        "--port",
143        type=int,
144        # default=8080,
145        help="port to serve on (default: 8080)",
146    )
147    parser.add_argument(
148        "-H",  # '-h' conflicts with --help
149        "--host",
150        help=(
151            "host to serve from (default: localhost). 'localhost' is only "
152            "accessible from the local computer. Use 0.0.0.0 to make your "
153            "application public"
154        ),
155    )
156    parser.add_argument(
157        "-r",
158        "--root",
159        dest="root_path",
160        action=FullExpandedPath,
161        help="path to a file system folder to publish for RW as share '/'.",
162    )
163    parser.add_argument(
164        "--auth",
165        choices=("anonymous", "nt", "pam-login"),
166        help="quick configuration of a domain controller when no config file "
167        "is used",
168    )
169    parser.add_argument(
170        "--server",
171        choices=SUPPORTED_SERVERS.keys(),
172        # default="cheroot",
173        help="type of pre-installed WSGI server to use (default: cheroot).",
174    )
175    parser.add_argument(
176        "--ssl-adapter",
177        choices=("builtin", "pyopenssl"),
178        # default="builtin",
179        help="used by 'cheroot' server if SSL certificates are configured "
180        "(default: builtin).",
181    )
182
183    qv_group = parser.add_mutually_exclusive_group()
184    qv_group.add_argument(
185        "-v",
186        "--verbose",
187        action="count",
188        default=3,
189        help="increment verbosity by one (default: %(default)s, range: 0..5)",
190    )
191    qv_group.add_argument(
192        "-q", "--quiet", default=0, action="count", help="decrement verbosity by one"
193    )
194
195    qv_group = parser.add_mutually_exclusive_group()
196    qv_group.add_argument(
197        "-c",
198        "--config",
199        dest="config_file",
200        action=FullExpandedPath,
201        help=(
202            f"configuration file (default: {DEFAULT_CONFIG_FILES} in current directory)"
203        ),
204    )
205
206    qv_group.add_argument(
207        "--no-config",
208        action="store_true",
209        help=f"do not try to load default {DEFAULT_CONFIG_FILES}",
210    )
211
212    parser.add_argument(
213        "--browse",
214        action="store_true",
215        help="open browser on start",
216    )
217
218    parser.add_argument(
219        "-V",
220        "--version",
221        action="store_true",
222        help="print version info and exit (may be combined with --verbose)",
223    )
224
225    args = parser.parse_args()
226
227    args.verbose -= args.quiet
228    del args.quiet
229
230    if args.root_path and not os.path.isdir(args.root_path):
231        msg = f"{args.root_path} is not a directory"
232        parser.error(msg)
233
234    if args.version:
235        if args.verbose >= 4:
236            version_info = "WsgiDAV/{} Python/{}({} bit) {}".format(
237                __version__,
238                util.PYTHON_VERSION,
239                "64" if sys.maxsize > 2**32 else "32",
240                platform.platform(aliased=True),
241            )
242            version_info += f"\nPython from: {sys.executable}"
243        else:
244            version_info = f"{__version__}"
245        print(version_info)
246        sys.exit()
247
248    if args.no_config:
249        pass
250        # ... else ignore default config files
251    elif args.config_file is None:
252        # If --config was omitted, use default (if it exists)
253        for filename in DEFAULT_CONFIG_FILES:
254            defPath = os.path.abspath(filename)
255            if os.path.exists(defPath):
256                if args.verbose >= 3:
257                    print(f"Using default configuration file: {defPath}")
258                args.config_file = defPath
259                break
260    else:
261        # If --config was specified convert to absolute path and assert it exists
262        args.config_file = os.path.abspath(args.config_file)
263        if not os.path.isfile(args.config_file):
264            parser.error(
265                "Could not find specified configuration file: {}".format(
266                    args.config_file
267                )
268            )
269
270    # Convert args object to dictionary
271    cmdLineOpts = args.__dict__.copy()
272    if args.verbose >= 5:
273        print("Command line args:")
274        for k, v in cmdLineOpts.items():
275            print("    {:>12}: {}".format(k, v))
276    return cmdLineOpts, parser
277
278
279def _read_config_file(config_file, _verbose):
280    """Read configuration file options into a dictionary."""
281
282    config_file = os.path.abspath(config_file)
283
284    if not os.path.exists(config_file):
285        raise RuntimeError(f"Couldn't open configuration file {config_file!r}.")
286
287    if config_file.endswith(".json"):
288        with open(config_file, mode="rt", encoding="utf-8-sig") as fp:
289            conf = json_load(fp)
290
291    elif config_file.endswith(".yaml"):
292        with open(config_file, mode="rt", encoding="utf-8-sig") as fp:
293            conf = yaml.safe_load(fp)
294
295    else:
296        raise RuntimeError(
297            f"Unsupported config file format (expected yaml or json): {config_file}"
298        )
299
300    conf["_config_file"] = config_file
301    conf["_config_root"] = os.path.dirname(config_file)
302    return conf
303
304
305def _init_config():
306    """Setup configuration dictionary from default, command line and configuration file."""
307    cli_opts, parser = _init_command_line_options()
308    cli_verbose = cli_opts["verbose"]
309
310    # Set config defaults
311    config = copy.deepcopy(DEFAULT_CONFIG)
312    config["_config_file"] = None
313    config["_config_root"] = os.getcwd()
314
315    # Configuration file overrides defaults
316    config_file = cli_opts.get("config_file")
317    if config_file:
318        file_opts = _read_config_file(config_file, cli_verbose)
319        util.deep_update(config, file_opts)
320        if cli_verbose != DEFAULT_VERBOSE and "verbose" in file_opts:
321            if cli_verbose >= 2:
322                print(
323                    "Config file defines 'verbose: {}' but is overridden by command line: {}.".format(
324                        file_opts["verbose"], cli_verbose
325                    )
326                )
327            config["verbose"] = cli_verbose
328    else:
329        if cli_verbose >= 2:
330            print("Running without configuration file.")
331
332    # Command line overrides file
333    if cli_opts.get("port"):
334        config["port"] = cli_opts.get("port")
335    if cli_opts.get("host"):
336        config["host"] = cli_opts.get("host")
337    if cli_opts.get("profile") is not None:
338        config["profile"] = True
339    if cli_opts.get("server") is not None:
340        config["server"] = cli_opts.get("server")
341    if cli_opts.get("ssl_adapter") is not None:
342        config["ssl_adapter"] = cli_opts.get("ssl_adapter")
343
344    # Command line overrides file only if -v or -q where passed:
345    if cli_opts.get("verbose") != DEFAULT_VERBOSE:
346        config["verbose"] = cli_opts.get("verbose")
347
348    if cli_opts.get("root_path"):
349        root_path = os.path.abspath(cli_opts.get("root_path"))
350        config["provider_mapping"]["/"] = FilesystemProvider(
351            root_path,
352            fs_opts=config.get("fs_dav_provider"),
353        )
354
355    if config["verbose"] >= 5:
356        # TODO: remove passwords from user_mapping
357        config_cleaned = util.purge_passwords(config)
358        print(
359            "Configuration({}):\n{}".format(
360                cli_opts["config_file"], pformat(config_cleaned)
361            )
362        )
363
364    if not config["provider_mapping"]:
365        parser.error("No DAV provider defined.")
366
367    # Quick-configuration of DomainController
368    auth = cli_opts.get("auth")
369    auth_conf = util.get_dict_value(config, "http_authenticator", as_dict=True)
370    if auth and auth_conf.get("domain_controller"):
371        parser.error(
372            "--auth option can only be used when no domain_controller is configured"
373        )
374
375    if auth == "anonymous":
376        if config["simple_dc"]["user_mapping"]:
377            parser.error(
378                "--auth=anonymous can only be used when no user_mapping is configured"
379            )
380        auth_conf.update(
381            {
382                "domain_controller": "wsgidav.dc.simple_dc.SimpleDomainController",
383                "accept_basic": True,
384                "accept_digest": True,
385                "default_to_digest": True,
386            }
387        )
388        config["simple_dc"]["user_mapping"] = {"*": True}
389    elif auth == "nt":
390        if config.get("nt_dc"):
391            parser.error(
392                "--auth=nt can only be used when no nt_dc settings are configured"
393            )
394        auth_conf.update(
395            {
396                "domain_controller": "wsgidav.dc.nt_dc.NTDomainController",
397                "accept_basic": True,
398                "accept_digest": False,
399                "default_to_digest": False,
400            }
401        )
402        config["nt_dc"] = {}
403    elif auth == "pam-login":
404        if config.get("pam_dc"):
405            parser.error(
406                "--auth=pam-login can only be used when no pam_dc settings are configured"
407            )
408        auth_conf.update(
409            {
410                "domain_controller": "wsgidav.dc.pam_dc.PAMDomainController",
411                "accept_basic": True,
412                "accept_digest": False,
413                "default_to_digest": False,
414            }
415        )
416        config["pam_dc"] = {"service": "login"}
417    # print(config)
418
419    # if cli_opts.get("reload"):
420    #     print("Installing paste.reloader.", file=sys.stderr)
421    #     from paste import reloader  # @UnresolvedImport
422
423    #     reloader.install()
424    #     if config_file:
425    #         # Add config file changes
426    #         reloader.watch_file(config_file)
427    #     # import pydevd
428    #     # pydevd.settrace()
429
430    if config["suppress_version_info"]:
431        util.public_wsgidav_info = "WsgiDAV"
432        util.public_python_info = f"Python/{sys.version_info[0]}"
433
434    return cli_opts, config
435
436
437def _run_cheroot(app, config, _server):
438    """Run WsgiDAV using cheroot.server (https://cheroot.cherrypy.dev/)."""
439    try:
440        from cheroot import server, wsgi
441    except ImportError:
442        _logger.exception("Could not import Cheroot (https://cheroot.cherrypy.dev/).")
443        _logger.error("Try `pip install cheroot`.")
444        return False
445
446    version = (
447        f"{util.public_wsgidav_info} {wsgi.Server.version} {util.public_python_info}"
448    )
449    # wsgi.Server.version = version
450
451    info = _get_common_info(config)
452
453    # Support SSL
454    if info["use_ssl"]:
455        ssl_adapter = info["ssl_adapter"]
456        ssl_adapter = server.get_ssl_adapter_class(ssl_adapter)
457        wsgi.Server.ssl_adapter = ssl_adapter(
458            info["ssl_cert"], info["ssl_pk"], info["ssl_chain"]
459        )
460        _logger.info("SSL / HTTPS enabled. Adapter: {}".format(ssl_adapter))
461
462    _logger.info(f"Running {version}")
463    _logger.info(f"Serving on {info['url']} ...")
464
465    server_args = {
466        "bind_addr": (config["host"], config["port"]),
467        "wsgi_app": app,
468        "server_name": version,
469        # File Explorer needs lot of threads (see issue #149):
470        "numthreads": 50,  # TODO: still required?
471    }
472    # Override or add custom args
473    custom_args = util.get_dict_value(config, "server_args", as_dict=True)
474    server_args.update(custom_args)
475
476    class PatchedServer(wsgi.Server):
477        STARTUP_NOTIFICATION_DELAY = 0.5
478
479        def serve(self, *args, **kwargs):
480            _logger.error("wsgi.Server.serve")
481            if startup_event and not startup_event.is_set():
482                Timer(self.STARTUP_NOTIFICATION_DELAY, startup_event.set).start()
483                _logger.error("wsgi.Server is ready")
484            return super().serve(*args, **kwargs)
485
486    # If the caller passed a startup event, monkey patch the server to set it
487    # when the request handler loop is entered
488    startup_event = config.get("startup_event")
489    if startup_event:
490        server = PatchedServer(**server_args)
491    else:
492        server = wsgi.Server(**server_args)
493
494    try:
495        server.start()
496    except KeyboardInterrupt:
497        _logger.warning("Caught Ctrl-C, shutting down...")
498    finally:
499        server.stop()
500
501    return
502
503
504def _run_ext_wsgiutils(app, config, _server):
505    """Run WsgiDAV using ext_wsgiutils_server from the wsgidav package."""
506    from wsgidav.server import ext_wsgiutils_server
507
508    _logger.warning(
509        "WARNING: This single threaded server (ext-wsgiutils) is not meant for production."
510    )
511    try:
512        ext_wsgiutils_server.serve(config, app)
513    except KeyboardInterrupt:
514        _logger.warning("Caught Ctrl-C, shutting down...")
515    return
516
517
518def _run_gevent(app, config, server):
519    """Run WsgiDAV using gevent if gevent (https://www.gevent.org).
520
521    See
522      https://github.com/gevent/gevent/blob/master/src/gevent/pywsgi.py#L1356
523      https://github.com/gevent/gevent/blob/master/src/gevent/server.py#L38
524    for more options.
525    """
526    try:
527        import gevent
528        import gevent.monkey
529        from gevent.pywsgi import WSGIServer
530    except ImportError:
531        _logger.exception("Could not import gevent (http://www.gevent.org).")
532        _logger.error("Try `pip install gevent`.")
533        return False
534
535    gevent.monkey.patch_all()
536
537    info = _get_common_info(config)
538    version = f"gevent/{gevent.__version__}"
539    version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
540
541    # Override or add custom args
542    server_args = {
543        "wsgi_app": app,
544        "bind_addr": (config["host"], config["port"]),
545    }
546    custom_args = util.get_dict_value(config, "server_args", as_dict=True)
547    server_args.update(custom_args)
548
549    if info["use_ssl"]:
550        dav_server = WSGIServer(
551            server_args["bind_addr"],
552            app,
553            keyfile=info["ssl_pk"],
554            certfile=info["ssl_cert"],
555            ca_certs=info["ssl_chain"],
556        )
557    else:
558        dav_server = WSGIServer(server_args["bind_addr"], app)
559
560    # If the caller passed a startup event, monkey patch the server to set it
561    # when the request handler loop is entered
562    startup_event = config.get("startup_event")
563    if startup_event:
564
565        def _patched_start():
566            dav_server.start_accepting = org_start  # undo the monkey patch
567            org_start()
568            _logger.info("gevent is ready")
569            startup_event.set()
570
571        org_start = dav_server.start_accepting
572        dav_server.start_accepting = _patched_start
573
574    _logger.info(f"Running {version}")
575    _logger.info(f"Serving on {info['url']} ...")
576    try:
577        gevent.spawn(dav_server.serve_forever())
578    except KeyboardInterrupt:
579        _logger.warning("Caught Ctrl-C, shutting down...")
580    return
581
582
583def _run_gunicorn(app, config, server):
584    """Run WsgiDAV using Gunicorn (https://gunicorn.org)."""
585    try:
586        import gunicorn.app.base
587    except ImportError:
588        _logger.exception("Could not import Gunicorn (https://gunicorn.org).")
589        _logger.error("Try `pip install gunicorn` (UNIX only).")
590        return False
591
592    info = _get_common_info(config)
593
594    class GunicornApplication(gunicorn.app.base.BaseApplication):
595        def __init__(self, app, options=None):
596            self.options = options or {}
597            self.application = app
598            super().__init__()
599
600        def load_config(self):
601            config = {
602                key: value
603                for key, value in self.options.items()
604                if key in self.cfg.settings and value is not None
605            }
606            for key, value in config.items():
607                self.cfg.set(key.lower(), value)
608
609        def load(self):
610            return self.application
611
612    # See https://docs.gunicorn.org/en/latest/settings.html
613    server_args = {
614        "bind": "{}:{}".format(config["host"], config["port"]),
615        "threads": 50,
616        "timeout": 1200,
617    }
618    if info["use_ssl"]:
619        server_args.update(
620            {
621                "keyfile": info["ssl_pk"],
622                "certfile": info["ssl_cert"],
623                "ca_certs": info["ssl_chain"],
624                # "ssl_version": ssl_version
625                # "cert_reqs": ssl_cert_reqs
626                # "ciphers": ssl_ciphers
627            }
628        )
629    # Override or add custom args
630    custom_args = util.get_dict_value(config, "server_args", as_dict=True)
631    server_args.update(custom_args)
632
633    version = f"gunicorn/{gunicorn.__version__}"
634    version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
635    _logger.info(f"Running {version} ...")
636
637    GunicornApplication(app, server_args).run()
638
639
640def _run_paste(app, config, server):
641    """Run WsgiDAV using paste.httpserver, if Paste is installed.
642
643    See http://pythonpaste.org/modules/httpserver.html for more options
644    """
645    try:
646        from paste import httpserver
647    except ImportError:
648        _logger.exception(
649            "Could not import paste.httpserver (https://github.com/cdent/paste)."
650        )
651        _logger.error("Try `pip install paste`.")
652        return False
653
654    info = _get_common_info(config)
655
656    version = httpserver.WSGIHandler.server_version
657    version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
658
659    # See http://pythonpaste.org/modules/httpserver.html for more options
660    server = httpserver.serve(
661        app,
662        host=config["host"],
663        port=config["port"],
664        server_version=version,
665        # This option enables handling of keep-alive and expect-100:
666        protocol_version="HTTP/1.1",
667        start_loop=False,
668    )
669
670    if config["verbose"] >= 5:
671        __handle_one_request = server.RequestHandlerClass.handle_one_request
672
673        def handle_one_request(self):
674            __handle_one_request(self)
675            if self.close_connection == 1:
676                _logger.debug("HTTP Connection : close")
677            else:
678                _logger.debug("HTTP Connection : continue")
679
680        server.RequestHandlerClass.handle_one_request = handle_one_request
681
682    _logger.info(f"Running {version} ...")
683    host, port = server.server_address
684    if host == "0.0.0.0":
685        _logger.info(f"Serving on 0.0.0.0:{port} view at http://127.0.0.1:{port}")
686    else:
687        _logger.info(f"Serving on {info['url']}")
688
689    try:
690        server.serve_forever()
691    except KeyboardInterrupt:
692        _logger.warning("Caught Ctrl-C, shutting down...")
693    return
694
695
696def _run_uvicorn(app, config, server):
697    """Run WsgiDAV using Uvicorn (https://www.uvicorn.org)."""
698    try:
699        import uvicorn
700    except ImportError:
701        _logger.exception("Could not import Uvicorn (https://www.uvicorn.org).")
702        _logger.error("Try `pip install uvicorn`.")
703        return False
704
705    info = _get_common_info(config)
706
707    # See https://www.uvicorn.org/settings/
708    server_args = {
709        "interface": "wsgi",
710        "host": config["host"],
711        "port": config["port"],
712        # TODO: see _run_cheroot()
713    }
714    if info["use_ssl"]:
715        server_args.update(
716            {
717                "ssl_keyfile": info["ssl_pk"],
718                "ssl_certfile": info["ssl_cert"],
719                "ssl_ca_certs": info["ssl_chain"],
720                # "ssl_keyfile_password": ssl_keyfile_password
721                # "ssl_version": ssl_version
722                # "ssl_cert_reqs": ssl_cert_reqs
723                # "ssl_ciphers": ssl_ciphers
724            }
725        )
726    # Override or add custom args
727    custom_args = util.get_dict_value(config, "server_args", as_dict=True)
728    server_args.update(custom_args)
729
730    version = f"uvicorn/{uvicorn.__version__}"
731    version = f"{util.public_wsgidav_info} {version} {util.public_python_info}"
732    _logger.info(f"Running {version} ...")
733
734    uvicorn.run(app, **server_args)
735
736
737def _run_wsgiref(app, config, _server):
738    """Run WsgiDAV using wsgiref.simple_server (https://docs.python.org/3/library/wsgiref.html)."""
739    from wsgiref.simple_server import WSGIRequestHandler, make_server
740
741    version = WSGIRequestHandler.server_version
742    version = f"{util.public_wsgidav_info} {version}"  # {util.public_python_info}"
743    _logger.info(f"Running {version} ...")
744
745    _logger.warning(
746        "WARNING: This single threaded server (wsgiref) is not meant for production."
747    )
748    WSGIRequestHandler.server_version = version
749    httpd = make_server(config["host"], config["port"], app)
750    # httpd.RequestHandlerClass.server_version = version
751    try:
752        httpd.serve_forever()
753    except KeyboardInterrupt:
754        _logger.warning("Caught Ctrl-C, shutting down...")
755    return
756
757
758SUPPORTED_SERVERS = {
759    "cheroot": _run_cheroot,
760    "ext-wsgiutils": _run_ext_wsgiutils,
761    "gevent": _run_gevent,
762    "gunicorn": _run_gunicorn,
763    "paste": _run_paste,
764    "uvicorn": _run_uvicorn,
765    "wsgiref": _run_wsgiref,
766}
767
768
769def run():
770    cli_opts, config = _init_config()
771
772    # util.init_logging(config) # now handled in constructor:
773    config["logging"]["enable"] = True
774
775    info = _get_common_info(config)
776
777    app = WsgiDAVApp(config)
778
779    server = config["server"]
780    handler = SUPPORTED_SERVERS.get(server)
781    if not handler:
782        raise RuntimeError(
783            "Unsupported server type {!r} (expected {!r})".format(
784                server, "', '".join(SUPPORTED_SERVERS.keys())
785            )
786        )
787
788    if not use_lxml and config["verbose"] >= 3:
789        _logger.warning(
790            "Could not import lxml: using xml instead (up to 10% slower). "
791            "Consider `pip install lxml`(see https://pypi.python.org/pypi/lxml)."
792        )
793
794    if cli_opts["browse"]:
795        BROWSE_DELAY = 2.0
796
797        def _worker():
798            url = info["url"]
799            url = url.replace("0.0.0.0", "127.0.0.1")
800            _logger.info(f"Starting browser on {url} ...")
801            webbrowser.open(url)
802
803        Timer(BROWSE_DELAY, _worker).start()
804
805    handler(app, config, server)
806    return
807
808
809if __name__ == "__main__":
810    # Just in case...
811    from multiprocessing import freeze_support
812
813    freeze_support()
814
815    run()