Source code for wsgidav.request_resolver

# -*- coding: utf-8 -*-
# (c) 2009-2022 Martin Wendt and contributors; see WsgiDAV https://github.com/mar10/wsgidav
# Original PyFileServer (c) 2005 Ho Chun Wei.
# Licensed under the MIT license:
# http://www.opensource.org/licenses/mit-license.php
"""
WSGI middleware that finds the registered mapped DAV-Provider, creates a new
RequestServer instance, and dispatches the request.

.. warning::
   The following documentation was taken over from PyFileServer and is outdated.

WsgiDAV file sharing
--------------------

WsgiDAV allows the user to specify in wsgidav.conf a number of
realms, and a number of users for each realm.

Realms
   Each realm corresponds to a filestructure on disk to be stored,
   for example::

      addShare('pubshare','/home/public/share')

   would allow the users to access using WebDAV the directory/file
   structure at /home/public/share from the url
   http://<servername:port>/<approot>/pubshare

   The realm name is set as '/pubshare'

   e.g. /home/public/share/WsgiDAV/LICENSE becomes accessible as
   http://<servername:port>/<approot>/pubshare/WsgiDAV/LICENSE

Users
   A number of user_name/password pairs can be set for each realm::

      adduser('pubshare', 'user_name', 'password', 'description/unused')

   would add a user_name/password pair to realm /pubshare.

Note: if developers wish to maintain a separate users database, you can
write your own domain controller for the HTTPAuthenticator. See
http_authenticator.py and domain_controller.py for more details.


Request Resolver
----------------

WSGI middleware for resolving Realm and Paths for the WsgiDAV
application.

Usage::
   It *must* be configured as the last item on `middleware_stack` list.

   from wsgidav.request_resolver import RequestResolver
   config = {
        ...,
        'middleware_stack': [
            ...,
            RequestResolver,
        ],
    }

The RequestResolver resolves the requested URL to the following values
placed in the environ dictionary. First it resolves the corresponding
realm::

   url: http://<servername:port>/<approot>/pubshare/WsgiDAV/LICENSE
   environ['wsgidav.mappedrealm'] = /pubshare

Based on the configuration given, the resource abstraction layer for the
realm is determined. if no configured abstraction layer is found, the
default abstraction layer fileabstractionlayer.FilesystemAbstractionLayer()
is used::

   environ['wsgidav.resourceAL'] = fileabstractionlayer.MyOwnFilesystemAbstractionLayer()

The path identifiers for the requested url are then resolved using the
resource abstraction layer::

   environ['wsgidav.mappedpath'] = /home/public/share/WsgiDAV/LICENSE
   environ['wsgidav.mappedURI'] = /pubshare/WsgiDAV/LICENSE

in this case, FilesystemAbstractionLayer resolves any relative paths
to its canonical absolute path

The RequestResolver also resolves any value in the Destination request
header, if present, to::

   Destination: http://<servername:port>/<approot>/pubshare/WsgiDAV/LICENSE-dest
   environ['wsgidav.destrealm'] = /pubshare
   environ['wsgidav.destpath'] = /home/public/share/WsgiDAV/LICENSE-dest
   environ['wsgidav.destURI'] = /pubshare/WsgiDAV/LICENSE
   environ['wsgidav.destresourceAL'] = fileabstractionlayer.MyOwnFilesystemAbstractionLayer()

"""
from wsgidav import util
from wsgidav.dav_error import HTTP_NOT_FOUND, DAVError
from wsgidav.mw.base_mw import BaseMiddleware
from wsgidav.request_server import RequestServer

__docformat__ = "reStructuredText"

_logger = util.get_module_logger(__name__)

# NOTE (Martin Wendt, 2009-05):
# The following remarks were made by Ian Bicking when reviewing PyFileServer in 2005.
# I leave them here after my refactoring for reference.
#
# Remarks:
# @@: If this were just generalized URL mapping, you'd map it like:
#    Incoming:
#        SCRIPT_NAME=<approot>; PATH_INFO=/pubshare/PyFileServer/LICENSE
#    After transforamtion:
#        SCRIPT_NAME=<approot>/pubshare; PATH_INFO=/PyFileServer/LICENSE
#    Then you dispatch to the application that serves '/home/public/share/'
#
#    This uses SCRIPT_NAME and PATH_INFO exactly how they are intended to be
#    used -- they give context about where you are (SCRIPT_NAME) and what you
#    still have to handle (PATH_INFO)
#
#    An example of an dispatcher that does this is paste.urlmap, and you use it
#    like:
#
#      urlmap = paste.urlmap.URLMap()
#      # urlmap is a WSGI application
#      urlmap['/pubshare'] = PyFileServerForPath('/home/public/share')
#
#    Now, that requires that you have a server that is easily
#    instantiated, but that's kind of a separate concern -- what you
#    really want is to do more general configuration at another level.  E.g.,
#    you might have::
#
#      app = config(urlmap, config_file)
#
#    Which adds the configuration from that file to the request, and
#    PyFileServerForPath then fetches that configuration.  paste.deploy
#    has another way of doing that at instantiation-time; either way
#    though you want to inherit configuration you can still use more general
#    dispatching.
#
#    Incidentally some WebDAV servers do redirection based on the user
#    agent (Zope most notably).  This is because of how WebDAV reuses
#    GET in an obnxious way, so that if you want to use WebDAV on pages
#    that also include dynamic content you have to mount the whole
#    thing at another point in the URL space, so you can GET the
#    content without rendering the dynamic parts.  I don't actually
#    like using user agents -- I'd rather mount the same resources at
#    two different URLs -- but it's just an example of another kind of
#    dispatching that can be done at a higher level.
#

# ========================================================================
# RequestResolver
# ========================================================================


[docs]class RequestResolver(BaseMiddleware): def __init__(self, wsgidav_app, next_app, config): super().__init__(wsgidav_app, next_app, config)
[docs] def __call__(self, environ, start_response): path = environ["PATH_INFO"] # We want to answer OPTIONS(*), even if no handler was registered for # the top-level realm (e.g. required to map drive letters). provider = environ["wsgidav.provider"] config = environ["wsgidav.config"] hotfixes = util.get_dict_value(config, "hotfixes", as_dict=True) is_asterisk_options = environ["REQUEST_METHOD"] == "OPTIONS" and path == "*" if path == "/": # Hotfix for WinXP / Vista: accept '/' for a '*' treat_as_asterisk = hotfixes.get("treat_root_options_as_asterisk") if treat_as_asterisk: is_asterisk_options = True else: _logger.info("Got OPTIONS '/' request") if is_asterisk_options: # Answer HTTP 'OPTIONS' method on server-level. # From RFC 2616: # If the Request-URI is an asterisk ("*"), the OPTIONS request is # intended to apply to the server in general rather than to a specific # resource. Since a server's communication options typically depend on # the resource, the "*" request is only useful as a "ping" or "no-op" # type of method; it does nothing beyond allowing the client to test the # capabilities of the server. For example, this can be used to test a # proxy for HTTP/1.1 compliance (or lack thereof). dav_compliance_level = "1,2" if ( provider is None or provider.is_readonly() or provider.lock_manager is None ): dav_compliance_level = "1" headers = [ ("Content-Type", "text/html; charset=utf-8"), ("Content-Length", "0"), ("DAV", dav_compliance_level), ("Date", util.get_rfc1123_time()), ] if environ["wsgidav.config"].get("add_header_MS_Author_Via", False): headers.append(("MS-Author-Via", "DAV")) start_response("200 OK", headers) yield b"" return if provider is None: raise DAVError( HTTP_NOT_FOUND, f"Could not find resource provider for {path!r}" ) # Let the appropriate resource provider for the realm handle the # request app = RequestServer(provider) app_iter = app(environ, start_response) for v in app_iter: yield v if hasattr(app_iter, "close"): app_iter.close() return