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    http://wsgidav.readthedocs.io/en/latest/run-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 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 = "{} is not a directory".format(args.root_path)
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 += "\nPython from: {}".format(sys.executable)
243        else:
244            version_info = "{}".format(__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("Using default configuration file: {}".format(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(root_path)
351
352    if config["verbose"] >= 5:
353        # TODO: remove passwords from user_mapping
354        config_cleaned = util.purge_passwords(config)
355        print(
356            "Configuration({}):\n{}".format(
357                cli_opts["config_file"], pformat(config_cleaned)
358            )
359        )
360
361    if not config["provider_mapping"]:
362        parser.error("No DAV provider defined.")
363
364    # Quick-configuration of DomainController
365    auth = cli_opts.get("auth")
366    auth_conf = util.get_dict_value(config, "http_authenticator", as_dict=True)
367    if auth and auth_conf.get("domain_controller"):
368        parser.error(
369            "--auth option can only be used when no domain_controller is configured"
370        )
371
372    if auth == "anonymous":
373        if config["simple_dc"]["user_mapping"]:
374            parser.error(
375                "--auth=anonymous can only be used when no user_mapping is configured"
376            )
377        auth_conf.update(
378            {
379                "domain_controller": "wsgidav.dc.simple_dc.SimpleDomainController",
380                "accept_basic": True,
381                "accept_digest": True,
382                "default_to_digest": True,
383            }
384        )
385        config["simple_dc"]["user_mapping"] = {"*": True}
386    elif auth == "nt":
387        if config.get("nt_dc"):
388            parser.error(
389                "--auth=nt can only be used when no nt_dc settings are configured"
390            )
391        auth_conf.update(
392            {
393                "domain_controller": "wsgidav.dc.nt_dc.NTDomainController",
394                "accept_basic": True,
395                "accept_digest": False,
396                "default_to_digest": False,
397            }
398        )
399        config["nt_dc"] = {}
400    elif auth == "pam-login":
401        if config.get("pam_dc"):
402            parser.error(
403                "--auth=pam-login can only be used when no pam_dc settings are configured"
404            )
405        auth_conf.update(
406            {
407                "domain_controller": "wsgidav.dc.pam_dc.PAMDomainController",
408                "accept_basic": True,
409                "accept_digest": False,
410                "default_to_digest": False,
411            }
412        )
413        config["pam_dc"] = {"service": "login"}
414    # print(config)
415
416    # if cli_opts.get("reload"):
417    #     print("Installing paste.reloader.", file=sys.stderr)
418    #     from paste import reloader  # @UnresolvedImport
419
420    #     reloader.install()
421    #     if config_file:
422    #         # Add config file changes
423    #         reloader.watch_file(config_file)
424    #     # import pydevd
425    #     # pydevd.settrace()
426
427    return cli_opts, config
428
429
430def _run_cheroot(app, config, _server):
431    """Run WsgiDAV using cheroot.server (https://cheroot.cherrypy.dev/)."""
432    try:
433        from cheroot import server, wsgi
434    except ImportError:
435        _logger.exception("Could not import Cheroot (https://cheroot.cherrypy.dev/).")
436        _logger.error("Try `pip install cheroot`.")
437        return False
438
439    version = wsgi.Server.version
440    version = f"WsgiDAV/{__version__} {version} Python {util.PYTHON_VERSION}"
441    wsgi.Server.version = version
442
443    info = _get_common_info(config)
444
445    # Support SSL
446    if info["use_ssl"]:
447        ssl_adapter = info["ssl_adapter"]
448        ssl_adapter = server.get_ssl_adapter_class(ssl_adapter)
449        wsgi.Server.ssl_adapter = ssl_adapter(
450            info["ssl_cert"], info["ssl_pk"], info["ssl_chain"]
451        )
452        _logger.info("SSL / HTTPS enabled. Adapter: {}".format(ssl_adapter))
453
454    _logger.info(f"Running {version}")
455    _logger.info(f"Serving on {info['url']} ...")
456
457    server_args = {
458        "bind_addr": (config["host"], config["port"]),
459        "wsgi_app": app,
460        "server_name": version,
461        # File Explorer needs lot of threads (see issue #149):
462        "numthreads": 50,  # TODO: still required?
463    }
464    # Override or add custom args
465    custom_args = util.get_dict_value(config, "server_args", as_dict=True)
466    server_args.update(custom_args)
467
468    class PatchedServer(wsgi.Server):
469        STARTUP_NOTIFICATION_DELAY = 0.5
470
471        def serve(self, *args, **kwargs):
472            _logger.error("wsgi.Server.serve")
473            if startup_event and not startup_event.is_set():
474                Timer(self.STARTUP_NOTIFICATION_DELAY, startup_event.set).start()
475                _logger.error("wsgi.Server is ready")
476            return super().serve(*args, **kwargs)
477
478    # If the caller passed a startup event, monkey patch the server to set it
479    # when the request handler loop is entered
480    startup_event = config.get("startup_event")
481    if startup_event:
482        server = PatchedServer(**server_args)
483    else:
484        server = wsgi.Server(**server_args)
485
486    try:
487        server.start()
488    except KeyboardInterrupt:
489        _logger.warning("Caught Ctrl-C, shutting down...")
490    finally:
491        server.stop()
492
493    return
494
495
496def _run_ext_wsgiutils(app, config, _server):
497    """Run WsgiDAV using ext_wsgiutils_server from the wsgidav package."""
498    from wsgidav.server import ext_wsgiutils_server
499
500    _logger.warning(
501        "WARNING: This single threaded server (ext-wsgiutils) is not meant for production."
502    )
503    try:
504        ext_wsgiutils_server.serve(config, app)
505    except KeyboardInterrupt:
506        _logger.warning("Caught Ctrl-C, shutting down...")
507    return
508
509
510# def _run_flup(app, config, server):
511#     """Run WsgiDAV using flup.server.fcgi (http://trac.saddi.com/flup/wiki/FlupServers)."""
512#     try:
513#         if server == "flup-fcgi":
514#             from flup.server.fcgi import WSGIServer
515#             from flup.server.fcgi import __version__ as flupver
516#         elif server == "flup-fcgi-fork":
517#             from flup.server.fcgi_fork import WSGIServer
518#             from flup.server.fcgi_fork import __version__ as flupver
519#         else:
520#             raise ValueError
521#     except ImportError:
522#         _logger.exception(f"Could not import {server} (https://gunicorn.org).")
523#         _logger.error("Try `pip install flup`.")
524#         return False
525
526#     version = f"{WSGIServer.__module__}/{flupver}"
527#     version = f"WsgiDAV/{__version__} {version} Python {util.PYTHON_VERSION}"
528#     _logger.info(f"Running {version} ...")
529
530#     server = WSGIServer(
531#         app,
532#         bindAddress=(config["host"], config["port"]),
533#         # debug=True,
534#     )
535#     try:
536#         server.run()
537#     except KeyboardInterrupt:
538#         _logger.warning("Caught Ctrl-C, shutting down...")
539#     return
540
541
542def _run_gevent(app, config, server):
543    """Run WsgiDAV using gevent if gevent (https://www.gevent.org).
544
545    See
546      https://github.com/gevent/gevent/blob/master/src/gevent/pywsgi.py#L1356
547      https://github.com/gevent/gevent/blob/master/src/gevent/server.py#L38
548    for more options.
549    """
550    try:
551        import gevent
552        import gevent.monkey
553        from gevent.pywsgi import WSGIServer
554    except ImportError:
555        _logger.exception("Could not import gevent (http://www.gevent.org).")
556        _logger.error("Try `pip install gevent`.")
557        return False
558
559    gevent.monkey.patch_all()
560
561    info = _get_common_info(config)
562    version = f"gevent/{gevent.__version__}"
563    version = f"WsgiDAV/{__version__} {version} Python {util.PYTHON_VERSION}"
564
565    # Override or add custom args
566    server_args = {
567        "wsgi_app": app,
568        "bind_addr": (config["host"], config["port"]),
569    }
570    custom_args = util.get_dict_value(config, "server_args", as_dict=True)
571    server_args.update(custom_args)
572
573    if info["use_ssl"]:
574        dav_server = WSGIServer(
575            server_args["bind_addr"],
576            app,
577            keyfile=info["ssl_pk"],
578            certfile=info["ssl_cert"],
579            ca_certs=info["ssl_chain"],
580        )
581    else:
582        dav_server = WSGIServer(server_args["bind_addr"], app)
583
584    # If the caller passed a startup event, monkey patch the server to set it
585    # when the request handler loop is entered
586    startup_event = config.get("startup_event")
587    if startup_event:
588
589        def _patched_start():
590            dav_server.start_accepting = org_start  # undo the monkey patch
591            org_start()
592            _logger.info("gevent is ready")
593            startup_event.set()
594
595        org_start = dav_server.start_accepting
596        dav_server.start_accepting = _patched_start
597
598    _logger.info(f"Running {version}")
599    _logger.info(f"Serving on {info['url']} ...")
600    try:
601        gevent.spawn(dav_server.serve_forever())
602    except KeyboardInterrupt:
603        _logger.warning("Caught Ctrl-C, shutting down...")
604    return
605
606
607def _run_gunicorn(app, config, server):
608    """Run WsgiDAV using Gunicorn (https://gunicorn.org)."""
609    try:
610        import gunicorn.app.base
611    except ImportError:
612        _logger.exception("Could not import Gunicorn (https://gunicorn.org).")
613        _logger.error("Try `pip install gunicorn` (UNIX only).")
614        return False
615
616    info = _get_common_info(config)
617
618    class GunicornApplication(gunicorn.app.base.BaseApplication):
619        def __init__(self, app, options=None):
620            self.options = options or {}
621            self.application = app
622            super().__init__()
623
624        def load_config(self):
625            config = {
626                key: value
627                for key, value in self.options.items()
628                if key in self.cfg.settings and value is not None
629            }
630            for key, value in config.items():
631                self.cfg.set(key.lower(), value)
632
633        def load(self):
634            return self.application
635
636    # See https://docs.gunicorn.org/en/latest/settings.html
637    server_args = {
638        "bind": "{}:{}".format(config["host"], config["port"]),
639        "threads": 50,
640        "timeout": 1200,
641    }
642    if info["use_ssl"]:
643        server_args.update(
644            {
645                "keyfile": info["ssl_pk"],
646                "certfile": info["ssl_cert"],
647                "ca_certs": info["ssl_chain"],
648                # "ssl_version": ssl_version
649                # "cert_reqs": ssl_cert_reqs
650                # "ciphers": ssl_ciphers
651            }
652        )
653    # Override or add custom args
654    custom_args = util.get_dict_value(config, "server_args", as_dict=True)
655    server_args.update(custom_args)
656
657    version = f"gunicorn/{gunicorn.__version__}"
658    version = f"WsgiDAV/{__version__} {version} Python {util.PYTHON_VERSION}"
659    _logger.info(f"Running {version} ...")
660
661    GunicornApplication(app, server_args).run()
662
663
664def _run_paste(app, config, server):
665    """Run WsgiDAV using paste.httpserver, if Paste is installed.
666
667    See http://pythonpaste.org/modules/httpserver.html for more options
668    """
669    try:
670        from paste import httpserver
671    except ImportError:
672        _logger.exception(
673            "Could not import paste.httpserver (https://github.com/cdent/paste)."
674        )
675        _logger.error("Try `pip install paste`.")
676        return False
677
678    info = _get_common_info(config)
679
680    version = httpserver.WSGIHandler.server_version
681    version = f"WsgiDAV/{__version__} {version} Python {util.PYTHON_VERSION}"
682
683    # See http://pythonpaste.org/modules/httpserver.html for more options
684    server = httpserver.serve(
685        app,
686        host=config["host"],
687        port=config["port"],
688        server_version=version,
689        # This option enables handling of keep-alive and expect-100:
690        protocol_version="HTTP/1.1",
691        start_loop=False,
692    )
693
694    if config["verbose"] >= 5:
695        __handle_one_request = server.RequestHandlerClass.handle_one_request
696
697        def handle_one_request(self):
698            __handle_one_request(self)
699            if self.close_connection == 1:
700                _logger.debug("HTTP Connection : close")
701            else:
702                _logger.debug("HTTP Connection : continue")
703
704        server.RequestHandlerClass.handle_one_request = handle_one_request
705
706    _logger.info(f"Running {version} ...")
707    host, port = server.server_address
708    if host == "0.0.0.0":
709        _logger.info(f"Serving on 0.0.0.0:{port} view at http://127.0.0.1:{port}")
710    else:
711        _logger.info(f"Serving on {info['url']}")
712
713    try:
714        server.serve_forever()
715    except KeyboardInterrupt:
716        _logger.warning("Caught Ctrl-C, shutting down...")
717    return
718
719
720def _run_uvicorn(app, config, server):
721    """Run WsgiDAV using Uvicorn (https://www.uvicorn.org)."""
722    try:
723        import uvicorn
724    except ImportError:
725        _logger.exception("Could not import Uvicorn (https://www.uvicorn.org).")
726        _logger.error("Try `pip install uvicorn`.")
727        return False
728
729    info = _get_common_info(config)
730
731    # See https://www.uvicorn.org/settings/
732    server_args = {
733        "interface": "wsgi",
734        "host": config["host"],
735        "port": config["port"],
736        # TODO: see _run_cheroot()
737    }
738    if info["use_ssl"]:
739        server_args.update(
740            {
741                "ssl_keyfile": info["ssl_pk"],
742                "ssl_certfile": info["ssl_cert"],
743                "ssl_ca_certs": info["ssl_chain"],
744                # "ssl_keyfile_password": ssl_keyfile_password
745                # "ssl_version": ssl_version
746                # "ssl_cert_reqs": ssl_cert_reqs
747                # "ssl_ciphers": ssl_ciphers
748            }
749        )
750    # Override or add custom args
751    custom_args = util.get_dict_value(config, "server_args", as_dict=True)
752    server_args.update(custom_args)
753
754    version = f"uvicorn/{uvicorn.__version__}"
755    version = f"WsgiDAV/{__version__} {version} Python {util.PYTHON_VERSION}"
756    _logger.info(f"Running {version} ...")
757
758    uvicorn.run(app, **server_args)
759
760
761def _run_wsgiref(app, config, _server):
762    """Run WsgiDAV using wsgiref.simple_server (https://docs.python.org/3/library/wsgiref.html)."""
763    from wsgiref.simple_server import WSGIRequestHandler, make_server
764
765    version = WSGIRequestHandler.server_version
766    version = f"WsgiDAV/{__version__} {version}"  # Python {util.PYTHON_VERSION}"
767    _logger.info(f"Running {version} ...")
768
769    _logger.warning(
770        "WARNING: This single threaded server (wsgiref) is not meant for production."
771    )
772    WSGIRequestHandler.server_version = version
773    httpd = make_server(config["host"], config["port"], app)
774    # httpd.RequestHandlerClass.server_version = version
775    try:
776        httpd.serve_forever()
777    except KeyboardInterrupt:
778        _logger.warning("Caught Ctrl-C, shutting down...")
779    return
780
781
782SUPPORTED_SERVERS = {
783    "cheroot": _run_cheroot,
784    # "cherrypy": _run__cherrypy,
785    "ext-wsgiutils": _run_ext_wsgiutils,
786    # "flup-fcgi_fork": _run_flup,
787    # "flup-fcgi": _run_flup,
788    "gevent": _run_gevent,
789    "gunicorn": _run_gunicorn,
790    "paste": _run_paste,
791    "uvicorn": _run_uvicorn,
792    "wsgiref": _run_wsgiref,
793}
794
795
796def run():
797    cli_opts, config = _init_config()
798
799    util.init_logging(config)
800
801    info = _get_common_info(config)
802
803    app = WsgiDAVApp(config)
804
805    server = config["server"]
806    handler = SUPPORTED_SERVERS.get(server)
807    if not handler:
808        raise RuntimeError(
809            "Unsupported server type {!r} (expected {!r})".format(
810                server, "', '".join(SUPPORTED_SERVERS.keys())
811            )
812        )
813
814    if not use_lxml and config["verbose"] >= 3:
815        _logger.warning(
816            "Could not import lxml: using xml instead (up to 10% slower). "
817            "Consider `pip install lxml`(see https://pypi.python.org/pypi/lxml)."
818        )
819
820    if cli_opts["browse"]:
821        BROWSE_DELAY = 2.0
822
823        def _worker():
824            url = info["url"]
825            url = url.replace("0.0.0.0", "127.0.0.1")
826            _logger.info(f"Starting browser on {url} ...")
827            webbrowser.open(url)
828
829        Timer(BROWSE_DELAY, _worker).start()
830
831    handler(app, config, server)
832    return
833
834
835if __name__ == "__main__":
836    # Just in case...
837    from multiprocessing import freeze_support
838
839    freeze_support()
840
841    run()