mirror of
https://github.com/YunoHost-Apps/seafile_ynh.git
synced 2024-09-03 20:26:01 +02:00
388 lines
17 KiB
Python
388 lines
17 KiB
Python
# (c) 2009-2011 Martin Wendt and contributors; see WsgiDAV http://wsgidav.googlecode.com/
|
|
# Original PyFileServer (c) 2005 Ho Chun Wei.
|
|
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
|
|
"""
|
|
WSGI container, that handles the HTTP requests. This object is passed to the
|
|
WSGI server and represents our WsgiDAV application to the outside.
|
|
|
|
On init:
|
|
|
|
Use the configuration dictionary to initialize lock manager, property manager,
|
|
domain controller.
|
|
|
|
Create a dictionary of share-to-provider mappings.
|
|
|
|
Initialize middleware objects and RequestResolver and setup the WSGI
|
|
application stack.
|
|
|
|
For every request:
|
|
|
|
Find the registered DAV provider for the current request.
|
|
|
|
Add or modify info in the WSGI ``environ``:
|
|
|
|
environ["SCRIPT_NAME"]
|
|
Mount-point of the current share.
|
|
environ["PATH_INFO"]
|
|
Resource path, relative to the mount path.
|
|
environ["wsgidav.provider"]
|
|
DAVProvider object that is registered for handling the current
|
|
request.
|
|
environ["wsgidav.config"]
|
|
Configuration dictionary.
|
|
environ["wsgidav.verbose"]
|
|
Debug level [0-3].
|
|
|
|
Log the HTTP request, then pass the request to the first middleware.
|
|
|
|
Note: The OPTIONS method for the '*' path is handled directly.
|
|
|
|
See `Developers info`_ for more information about the WsgiDAV architecture.
|
|
|
|
.. _`Developers info`: http://docs.wsgidav.googlecode.com/hg/html/develop.html
|
|
"""
|
|
from fs_dav_provider import FilesystemProvider
|
|
from wsgidav.dir_browser import WsgiDavDirBrowser
|
|
from wsgidav.dav_provider import DAVProvider
|
|
from wsgidav.lock_storage import LockStorageDict
|
|
import time
|
|
import sys
|
|
import threading
|
|
import urllib
|
|
import util
|
|
from error_printer import ErrorPrinter
|
|
from debug_filter import WsgiDavDebugFilter
|
|
from http_authenticator import HTTPAuthenticator
|
|
from request_resolver import RequestResolver
|
|
from domain_controller import WsgiDAVDomainController
|
|
from property_manager import PropertyManager
|
|
from lock_manager import LockManager
|
|
#from wsgidav.version import __version__
|
|
|
|
__docformat__ = "reStructuredText"
|
|
|
|
|
|
# Use these settings, if config file does not define them (or is totally missing)
|
|
DEFAULT_CONFIG = {
|
|
"mount_path": None, # Application root, e.g. <mount_path>/<share_name>/<res_path>
|
|
"provider_mapping": {},
|
|
"host": "localhost",
|
|
"port": 8080,
|
|
"ext_servers": [
|
|
# "paste",
|
|
# "cherrypy",
|
|
# "wsgiref",
|
|
"cherrypy-bundled",
|
|
"wsgidav",
|
|
],
|
|
|
|
"enable_loggers": [
|
|
],
|
|
|
|
"propsmanager": None, # True: use property_manager.PropertyManager
|
|
"locksmanager": True, # True: use lock_manager.LockManager
|
|
|
|
# HTTP Authentication Options
|
|
"user_mapping": {}, # dictionary of dictionaries
|
|
"domaincontroller": None, # None: domain_controller.WsgiDAVDomainController(user_mapping)
|
|
"acceptbasic": True, # Allow basic authentication, True or False
|
|
"acceptdigest": True, # Allow digest authentication, True or False
|
|
"defaultdigest": True, # True (default digest) or False (default basic)
|
|
|
|
# Verbose Output
|
|
"verbose": 2, # 0 - no output (excepting application exceptions)
|
|
# 1 - show single line request summaries (for HTTP logging)
|
|
# 2 - show additional events
|
|
# 3 - show full request/response header info (HTTP Logging)
|
|
# request body and GET response bodies not shown
|
|
|
|
"dir_browser": {
|
|
"enable": True, # Render HTML listing for GET requests on collections
|
|
"response_trailer": "", # Raw HTML code, appended as footer
|
|
"davmount": False, # Send <dm:mount> response if request URL contains '?davmount'
|
|
"msmount": False, # Add an 'open as webfolder' link (requires Windows)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
def _checkConfig(config):
|
|
mandatoryFields = ["provider_mapping",
|
|
]
|
|
for field in mandatoryFields:
|
|
if not field in config:
|
|
raise ValueError("Invalid configuration: missing required field '%s'" % field)
|
|
|
|
|
|
|
|
|
|
#===============================================================================
|
|
# WsgiDAVApp
|
|
#===============================================================================
|
|
class WsgiDAVApp(object):
|
|
|
|
def __init__(self, config):
|
|
self.config = config
|
|
|
|
util.initLogging(config["verbose"],
|
|
config.get("log_path", ""),
|
|
config.get("enable_loggers", []))
|
|
|
|
util.log("Default encoding: %s (file system: %s)" % (sys.getdefaultencoding(), sys.getfilesystemencoding()))
|
|
|
|
# Evaluate configuration and set defaults
|
|
_checkConfig(config)
|
|
provider_mapping = self.config["provider_mapping"]
|
|
# response_trailer = config.get("response_trailer", "")
|
|
self._verbose = config.get("verbose", 2)
|
|
|
|
lockStorage = config.get("locksmanager")
|
|
if lockStorage is True:
|
|
lockStorage = LockStorageDict()
|
|
|
|
if not lockStorage:
|
|
locksManager = None
|
|
else:
|
|
locksManager = LockManager(lockStorage)
|
|
|
|
propsManager = config.get("propsmanager")
|
|
if not propsManager:
|
|
# Normalize False, 0 to None
|
|
propsManager = None
|
|
elif propsManager is True:
|
|
propsManager = PropertyManager()
|
|
|
|
mount_path = config.get("mount_path")
|
|
|
|
user_mapping = self.config.get("user_mapping", {})
|
|
domainController = config.get("domaincontroller") or WsgiDAVDomainController(user_mapping)
|
|
isDefaultDC = isinstance(domainController, WsgiDAVDomainController)
|
|
|
|
# authentication fields
|
|
authacceptbasic = config.get("acceptbasic", True)
|
|
authacceptdigest = config.get("acceptdigest", True)
|
|
authdefaultdigest = config.get("defaultdigest", True)
|
|
|
|
# Check configuration for NTDomainController
|
|
# We don't use 'isinstance', because include would fail on non-windows boxes.
|
|
wdcName = "NTDomainController"
|
|
if domainController.__class__.__name__ == wdcName:
|
|
if authacceptdigest or authdefaultdigest or not authacceptbasic:
|
|
util.warn("WARNING: %s requires basic authentication.\n\tSet acceptbasic=True, acceptdigest=False, defaultdigest=False" % wdcName)
|
|
|
|
# Instantiate DAV resource provider objects for every share
|
|
self.providerMap = {}
|
|
for (share, provider) in provider_mapping.items():
|
|
# Make sure share starts with, or is, '/'
|
|
share = "/" + share.strip("/")
|
|
|
|
# We allow a simple string as 'provider'. In this case we interpret
|
|
# it as a file system root folder that is published.
|
|
if isinstance(provider, basestring):
|
|
provider = FilesystemProvider(provider)
|
|
|
|
assert isinstance(provider, DAVProvider)
|
|
|
|
provider.setSharePath(share)
|
|
if mount_path:
|
|
provider.setMountPath(mount_path)
|
|
|
|
# TODO: someday we may want to configure different lock/prop managers per provider
|
|
provider.setLockManager(locksManager)
|
|
provider.setPropManager(propsManager)
|
|
|
|
self.providerMap[share] = provider
|
|
|
|
|
|
if self._verbose >= 2:
|
|
print "Using lock manager: %r" % locksManager
|
|
print "Using property manager: %r" % propsManager
|
|
print "Using domain controller: %s" % domainController
|
|
print "Registered DAV providers:"
|
|
for share, provider in self.providerMap.items():
|
|
hint = ""
|
|
if isDefaultDC and not user_mapping.get(share):
|
|
hint = " (anonymous)"
|
|
print " Share '%s': %s%s" % (share, provider, hint)
|
|
|
|
# If the default DC is used, emit a warning for anonymous realms
|
|
if isDefaultDC and self._verbose >= 1:
|
|
for share in self.providerMap:
|
|
if not user_mapping.get(share):
|
|
# TODO: we should only warn here, if --no-auth is not given
|
|
print "WARNING: share '%s' will allow anonymous access." % share
|
|
|
|
# Define WSGI application stack
|
|
application = RequestResolver()
|
|
|
|
if config.get("dir_browser") and config["dir_browser"].get("enable", True):
|
|
application = config["dir_browser"].get("app_class", WsgiDavDirBrowser)(application)
|
|
|
|
application = HTTPAuthenticator(application,
|
|
domainController,
|
|
authacceptbasic,
|
|
authacceptdigest,
|
|
authdefaultdigest)
|
|
application = ErrorPrinter(application, catchall=True)
|
|
|
|
application = WsgiDavDebugFilter(application, config)
|
|
|
|
self._application = application
|
|
|
|
|
|
def __call__(self, environ, start_response):
|
|
|
|
# util.log("SCRIPT_NAME='%s', PATH_INFO='%s'" % (environ.get("SCRIPT_NAME"), environ.get("PATH_INFO")))
|
|
|
|
# We unquote PATH_INFO here, although this should already be done by
|
|
# the server.
|
|
path = urllib.unquote(environ["PATH_INFO"])
|
|
# issue 22: Pylons sends root as u'/'
|
|
if isinstance(path, unicode):
|
|
util.log("Got unicode PATH_INFO: %r" % path)
|
|
path = path.encode("utf8")
|
|
|
|
# Always adding these values to environ:
|
|
environ["wsgidav.config"] = self.config
|
|
environ["wsgidav.provider"] = None
|
|
environ["wsgidav.verbose"] = self._verbose
|
|
|
|
## Find DAV provider that matches the share
|
|
|
|
# sorting share list by reverse length
|
|
shareList = self.providerMap.keys()
|
|
shareList.sort(key=len, reverse=True)
|
|
|
|
share = None
|
|
for r in shareList:
|
|
# @@: Case sensitivity should be an option of some sort here;
|
|
# os.path.normpath might give the preferred case for a filename.
|
|
if r == "/":
|
|
share = r
|
|
break
|
|
elif path.upper() == r.upper() or path.upper().startswith(r.upper()+"/"):
|
|
share = r
|
|
break
|
|
|
|
provider = self.providerMap.get(share)
|
|
|
|
# Note: we call the next app, even if provider is None, because OPTIONS
|
|
# must still be handled.
|
|
# All other requests will result in '404 Not Found'
|
|
environ["wsgidav.provider"] = provider
|
|
|
|
# TODO: test with multi-level realms: 'aa/bb'
|
|
# TODO: test security: url contains '..'
|
|
|
|
# Transform SCRIPT_NAME and PATH_INFO
|
|
# (Since path and share are unquoted, this also fixes quoted values.)
|
|
if share == "/" or not share:
|
|
environ["PATH_INFO"] = path
|
|
else:
|
|
environ["SCRIPT_NAME"] += share
|
|
environ["PATH_INFO"] = path[len(share):]
|
|
# util.log("--> SCRIPT_NAME='%s', PATH_INFO='%s'" % (environ.get("SCRIPT_NAME"), environ.get("PATH_INFO")))
|
|
|
|
assert isinstance(path, str)
|
|
# See http://mail.python.org/pipermail/web-sig/2007-January/002475.html
|
|
# for some clarification about SCRIPT_NAME/PATH_INFO format
|
|
# SCRIPT_NAME starts with '/' or is empty
|
|
assert environ["SCRIPT_NAME"] == "" or environ["SCRIPT_NAME"].startswith("/")
|
|
# SCRIPT_NAME must not have a trailing '/'
|
|
assert environ["SCRIPT_NAME"] in ("", "/") or not environ["SCRIPT_NAME"].endswith("/")
|
|
# PATH_INFO starts with '/'
|
|
assert environ["PATH_INFO"] == "" or environ["PATH_INFO"].startswith("/")
|
|
|
|
start_time = time.time()
|
|
def _start_response_wrapper(status, response_headers, exc_info=None):
|
|
# Postprocess response headers
|
|
headerDict = {}
|
|
for header, value in response_headers:
|
|
if header.lower() in headerDict:
|
|
util.warn("Duplicate header in response: %s" % header)
|
|
headerDict[header.lower()] = value
|
|
|
|
# Check if we should close the connection after this request.
|
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4
|
|
forceCloseConnection = False
|
|
currentContentLength = headerDict.get("content-length")
|
|
statusCode = int(status.split(" ", 1)[0])
|
|
contentLengthRequired = (environ["REQUEST_METHOD"] != "HEAD"
|
|
and statusCode >= 200
|
|
and not statusCode in (204, 304))
|
|
# print environ["REQUEST_METHOD"], statusCode, contentLengthRequired
|
|
if contentLengthRequired and currentContentLength in (None, ""):
|
|
# A typical case: a GET request on a virtual resource, for which
|
|
# the provider doesn't know the length
|
|
util.warn("Missing required Content-Length header in %s-response: closing connection" % statusCode)
|
|
forceCloseConnection = True
|
|
elif not type(currentContentLength) is str:
|
|
util.warn("Invalid Content-Length header in response (%r): closing connection" % headerDict.get("content-length"))
|
|
forceCloseConnection = True
|
|
|
|
# HOTFIX for Vista and Windows 7 (issue 13, issue 23)
|
|
# It seems that we must read *all* of the request body, otherwise
|
|
# clients may miss the response.
|
|
# For example Vista MiniRedir didn't understand a 401 response,
|
|
# when trying an anonymous PUT of big files. As a consequence, it
|
|
# doesn't retry with credentials and the file copy fails.
|
|
# (XP is fine however).
|
|
util.readAndDiscardInput(environ)
|
|
|
|
# Make sure the socket is not reused, unless we are 100% sure all
|
|
# current input was consumed
|
|
if(util.getContentLength(environ) != 0
|
|
and not environ.get("wsgidav.all_input_read")):
|
|
util.warn("Input stream not completely consumed: closing connection")
|
|
forceCloseConnection = True
|
|
|
|
if forceCloseConnection and headerDict.get("connection") != "close":
|
|
util.warn("Adding 'Connection: close' header")
|
|
response_headers.append(("Connection", "close"))
|
|
|
|
# Log request
|
|
if self._verbose >= 1:
|
|
userInfo = environ.get("http_authenticator.username")
|
|
if not userInfo:
|
|
userInfo = "(anonymous)"
|
|
threadInfo = ""
|
|
if self._verbose >= 1:
|
|
threadInfo = "<%s> " % threading._get_ident()
|
|
extra = []
|
|
if "HTTP_DESTINATION" in environ:
|
|
extra.append('dest="%s"' % environ.get("HTTP_DESTINATION"))
|
|
if environ.get("CONTENT_LENGTH", "") != "":
|
|
extra.append("length=%s" % environ.get("CONTENT_LENGTH"))
|
|
if "HTTP_DEPTH" in environ:
|
|
extra.append("depth=%s" % environ.get("HTTP_DEPTH"))
|
|
if "HTTP_RANGE" in environ:
|
|
extra.append("range=%s" % environ.get("HTTP_RANGE"))
|
|
if "HTTP_OVERWRITE" in environ:
|
|
extra.append("overwrite=%s" % environ.get("HTTP_OVERWRITE"))
|
|
if self._verbose >= 1 and "HTTP_EXPECT" in environ:
|
|
extra.append('expect="%s"' % environ.get("HTTP_EXPECT"))
|
|
if self._verbose >= 2 and "HTTP_CONNECTION" in environ:
|
|
extra.append('connection="%s"' % environ.get("HTTP_CONNECTION"))
|
|
if self._verbose >= 2 and "HTTP_USER_AGENT" in environ:
|
|
extra.append('agent="%s"' % environ.get("HTTP_USER_AGENT"))
|
|
if self._verbose >= 2 and "HTTP_TRANSFER_ENCODING" in environ:
|
|
extra.append('transfer-enc=%s' % environ.get("HTTP_TRANSFER_ENCODING"))
|
|
if self._verbose >= 1:
|
|
extra.append('elap=%.3fsec' % (time.time() - start_time))
|
|
extra = ", ".join(extra)
|
|
|
|
util.log('%s - %s - "%s" %s -> %s' % (
|
|
environ.get("REMOTE_ADDR",""),
|
|
userInfo,
|
|
environ.get("REQUEST_METHOD") + " " + environ.get("PATH_INFO", ""),
|
|
extra,
|
|
status
|
|
))
|
|
|
|
return start_response(status, response_headers, exc_info)
|
|
|
|
# Call next middleware
|
|
for v in self._application(environ, _start_response_wrapper):
|
|
yield v
|
|
return
|