def validate_lock(lock): assert compat.is_native(lock["root"]) assert lock["root"].startswith("/") assert lock["type"] == "write" assert lock["scope"] in ("shared", "exclusive") assert lock["depth"] in ("0", "infinity") assert compat.is_bytes(lock["owner"]), lock # XML bytestring # raises TypeError: timeout = float(lock["timeout"]) assert timeout > 0 or timeout == -1, "timeout must be positive or -1" assert compat.is_native(lock["principal"]) if "token" in lock: assert compat.is_native(lock["token"])
def _loc_to_file_path(self, path, environ=None): """Convert resource path to a unicode absolute file path. Optional environ argument may be useful e.g. in relation to per-user sub-folder chrooting inside root_folder_path. """ root_path = self.root_folder_path assert root_path is not None assert compat.is_native(root_path) assert compat.is_native(path) path_parts = path.strip("/").split("/") file_path = os.path.abspath(os.path.join(root_path, *path_parts)) if not file_path.startswith(root_path): raise RuntimeError( "Security exception: tried to access file outside root: {}".format( file_path ) ) # Convert to unicode file_path = util.to_unicode_safe(file_path) return file_path
def get_lock_list(self, path, include_root, include_children, token_only): """Return a list of direct locks for <path>. Expired locks are *not* returned (but may be purged). path: Normalized path (utf8 encoded string, no trailing '/') include_root: False: don't add <path> lock (only makes sense, when include_children is True). include_children: True: Also check all sub-paths for existing locks. token_only: True: only a list of token is returned. This may be implemented more efficiently by some providers. Returns: List of valid lock dictionaries (may be empty). """ assert compat.is_native(path) assert path and path.startswith("/") assert include_root or include_children def __appendLocks(toklist): # Since we can do this quickly, we use self.get() even if # token_only is set, so expired locks are purged. for token in toklist: lock = self.get(token) if lock: if token_only: lockList.append(lock["token"]) else: lockList.append(lock) path = normalize_lock_root(path) self._lock.acquire_read() try: key = "URL2TOKEN:{}".format(path) tokList = self._dict.get(key, []) lockList = [] if include_root: __appendLocks(tokList) if include_children: for u, ltoks in self._dict.items(): if util.is_child_uri(key, u): __appendLocks(ltoks) return lockList finally: self._lock.release()
def __init__( self, status_code, context_info=None, src_exception=None, err_condition=None ): # allow passing of Pre- and Postconditions, see # http://www.webdav.org/specs/rfc4918.html#precondition.postcondition.xml.elements self.value = int(status_code) self.context_info = context_info self.src_exception = src_exception self.err_condition = err_condition if compat.is_native(err_condition): self.err_condition = DAVErrorCondition(err_condition) assert ( self.err_condition is None or type(self.err_condition) is DAVErrorCondition )
def get_lock_list(self, path, include_root, include_children, token_only): """Return a list of direct locks for <path>. Expired locks are *not* returned (but may be purged). path: Normalized path (utf8 encoded string, no trailing '/') include_root: False: don't add <path> lock (only makes sense, when include_children is True). include_children: True: Also check all sub-paths for existing locks. token_only: True: only a list of token is returned. This may be implemented more efficiently by some providers. Returns: List of valid lock dictionaries (may be empty). """ assert compat.is_native(path) assert path and path.startswith("/") assert include_root or include_children def __appendLocks(toklist): # Since we can do this quickly, we use self.get() even if # token_only is set, so expired locks are purged. for token in map(lambda x: x.decode("utf-8"), toklist): lock = self._redis.get(self._redis_lock_prefix.format(token)) if lock: lock = pickle.loads(lock) if token_only: lockList.append(lock["token"]) else: lockList.append(lock) path = normalize_lock_root(path) key = self._redis_url2token_prefix.format(path) tokList = self._redis.lrange(key, 0, -1) lockList = [] if include_root: __appendLocks(tokList) if include_children: for u in map(lambda x: x.decode("utf-8"), self._redis.keys(key + "/*")): if util.is_child_uri(key, u): __appendLocks(self._redis.lrange(u, 0, -1)) return lockList
def get_member(self, name): """Return direct collection member (DAVResource or derived). See DAVCollection.get_member() """ assert compat.is_native(name), "{!r}".format(name) fp = os.path.join(self._file_path, compat.to_unicode(name)) # name = name.encode("utf8") path = util.join_uri(self.path, name) if os.path.isdir(fp): res = FolderResource(path, self.environ, fp) elif os.path.isfile(fp): res = FileResource(path, self.environ, fp) else: _logger.debug("Skipping non-file {}".format(path)) res = None return res
def get_member(self, name): """Return direct collection member (DAVResource or derived). See DAVCollection.get_member() """ assert compat.is_native(name), "{!r}".format(name) fp = os.path.join(self._file_path, compat.to_unicode(name)) # name = name.encode("utf8") path = util.join_uri(self.path, name) if os.path.isdir(fp): res = FolderResource(path, self.environ, fp) elif os.path.isfile(fp): res = FileResource(path, self.environ, fp) else: _logger.debug("Skipping non-file {}".format(path)) res = None return res
def check_write_permission(self, url, depth, tokenList, principal): """Check, if <principal> can modify <url>, otherwise raise HTTP_LOCKED. If modifying <url> is prevented by a lock, DAVError(HTTP_LOCKED) is raised. An embedded DAVErrorCondition contains the conflicting locks. <url> may be modified by <principal>, if it is not currently locked directly or indirectly (i.e. by a locked parent). For depth-infinity operations, <url> also must not have locked children. It is not enough to check whether a lock is owned by <principal>, but also the token must be passed with the request. Because <principal> may run two different applications. See http://www.webdav.org/specs/rfc4918.html#lock-model http://www.webdav.org/specs/rfc4918.html#rfc.section.7.4 TODO: verify assumptions: - Parent locks WILL NOT be conflicting, if they are depth-0. - Exclusive child locks WILL be conflicting, even if they are owned by <principal>. @param url: URL that shall be modified, created, moved, or deleted @param depth: "0"|"infinity" @param tokenList: list of lock tokens, that the principal submitted in If: header @param principal: name of the principal requesting a lock @return: None or raise error """ assert compat.is_native(url) assert depth in ("0", "infinity") _logger.debug("check_write_permission({}, {}, {}, {})".format( url, depth, tokenList, principal)) # Error precondition to collect conflicting URLs errcond = DAVErrorCondition(PRECONDITION_CODE_LockConflict) self._lock.acquire_read() try: # Check url and all parents for conflicting locks u = url while u: ll = self.get_url_lock_list(u) _logger.debug(" checking {}".format(u)) for l in ll: _logger.debug(" l={}".format(lock_string(l))) if u != url and l["depth"] != "infinity": # We only consider parents with Depth: inifinity continue elif principal == l["principal"] and l[ "token"] in tokenList: # User owns this lock continue else: # Token is owned by principal, but not passed with lock list _logger.debug( " -> DENIED due to locked parent {}".format( lock_string(l))) errcond.add_href(l["root"]) u = util.get_uri_parent(u) if depth == "infinity": # Check child URLs for conflicting locks childLocks = self.storage.get_lock_list(url, includeRoot=False, includeChildren=True, tokenOnly=False) for l in childLocks: assert util.is_child_uri(url, l["root"]) # if util.is_child_uri(url, l["root"]): _logger.debug(" -> DENIED due to locked child {}".format( lock_string(l))) errcond.add_href(l["root"]) finally: self._lock.release() # If there were conflicts, raise HTTP_LOCKED for <url>, and pass # conflicting resource with 'no-conflicting-lock' precondition if len(errcond.hrefs) > 0: raise DAVError(HTTP_LOCKED, errcondition=errcond) return
def __call__(self, environ, start_response): # util.log("SCRIPT_NAME='{}', PATH_INFO='{}'".format( # environ.get("SCRIPT_NAME"), environ.get("PATH_INFO"))) path = environ["PATH_INFO"] # (#73) Failed on processing non-iso-8859-1 characters on Python 3 # # Note: we encode using UTF-8 here (falling back to ISO-8859-1)! # This seems to be wrong, since per PEP 3333 PATH_INFO is always ISO-8859-1 encoded # (see https://www.python.org/dev/peps/pep-3333/#unicode-issues). # But also seems to resolve errors when accessing resources with Chinese characters, for # example. # This is done by default for Python 3, but can be turned off in settings. re_encode_path_info = self.config.get("re_encode_path_info") if re_encode_path_info is None: re_encode_path_info = compat.PY3 if re_encode_path_info: path = environ["PATH_INFO"] = compat.wsgi_to_bytes(path).decode() # We optionally unquote PATH_INFO here, although this should already be # done by the server (#8). if self.config.get("unquote_path_info", False): path = compat.unquote(environ["PATH_INFO"]) # GC issue 22: Pylons sends root as u'/' if not compat.is_native(path): _logger.warn("Got non-native PATH_INFO: {!r}".format(path)) # path = path.encode("utf8") path = compat.to_native(path) # 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 share = None lower_path = path.lower() for r in self.sortedShareList: # @@: 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 lower_path == r or lower_path.startswith(r + "/"): share = r break # 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' if share is not None: share_data = self.providerMap.get(share) environ["wsgidav.provider"] = share_data["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) :] # assert isinstance(path, str) assert compat.is_native(path) # 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: _logger.error("Duplicate header in response: {}".format(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 statusCode not in (204, 304) ) # _logger.info(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 _logger.error( "Missing required Content-Length header in {}-response: closing connection".format( statusCode ) ) forceCloseConnection = True elif not type(currentContentLength) is str: _logger.error( "Invalid Content-Length header in response ({!r}): closing connection".format( headerDict.get("content-length") ) ) forceCloseConnection = True # HOTFIX for Vista and Windows 7 (GC 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.read_and_discard_input(environ) # Make sure the socket is not reused, unless we are 100% sure all # current input was consumed if util.get_content_length(environ) != 0 and not environ.get( "wsgidav.all_input_read" ): _logger.warn("Input stream not completely consumed: closing connection") forceCloseConnection = True if forceCloseConnection and headerDict.get("connection") != "close": _logger.warn("Adding 'Connection: close' header") response_headers.append(("Connection", "close")) # Log request if self.verbose >= 3: userInfo = environ.get("http_authenticator.username") if not userInfo: userInfo = "(anonymous)" extra = [] if "HTTP_DESTINATION" in environ: extra.append('dest="{}"'.format(environ.get("HTTP_DESTINATION"))) if environ.get("CONTENT_LENGTH", "") != "": extra.append("length={}".format(environ.get("CONTENT_LENGTH"))) if "HTTP_DEPTH" in environ: extra.append("depth={}".format(environ.get("HTTP_DEPTH"))) if "HTTP_RANGE" in environ: extra.append("range={}".format(environ.get("HTTP_RANGE"))) if "HTTP_OVERWRITE" in environ: extra.append("overwrite={}".format(environ.get("HTTP_OVERWRITE"))) if self.verbose >= 3 and "HTTP_EXPECT" in environ: extra.append('expect="{}"'.format(environ.get("HTTP_EXPECT"))) if self.verbose >= 4 and "HTTP_CONNECTION" in environ: extra.append( 'connection="{}"'.format(environ.get("HTTP_CONNECTION")) ) if self.verbose >= 4 and "HTTP_USER_AGENT" in environ: extra.append('agent="{}"'.format(environ.get("HTTP_USER_AGENT"))) if self.verbose >= 4 and "HTTP_TRANSFER_ENCODING" in environ: extra.append( "transfer-enc={}".format(environ.get("HTTP_TRANSFER_ENCODING")) ) if self.verbose >= 3: extra.append("elap={:.3f}sec".format(time.time() - start_time)) extra = ", ".join(extra) # This is the CherryPy format: # 127.0.0.1 - - [08/Jul/2009:17:25:23] "GET /loginPrompt?redirect=/renderActionList%3Frelation%3Dpersonal%26key%3D%26filter%3DprivateSchedule&reason=0 HTTP/1.1" 200 1944 "http://127.0.0.1:8002/command?id=CMD_Schedule" "Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.1) Gecko/20090624 Firefox/3.5" # noqa _logger.info( '{addr} - {user} - [{time}] "{method} {path}" {extra} -> {status}'.format( addr=environ.get("REMOTE_ADDR", ""), user=userInfo, time=util.get_log_time(), method=environ.get("REQUEST_METHOD"), path=safe_re_encode( environ.get("PATH_INFO", ""), sys.stdout.encoding ), extra=extra, status=status, # response_headers.get(""), # response Content-Length # referer ) ) return start_response(status, response_headers, exc_info) # Call first middleware app_iter = self.application(environ, _start_response_wrapper) try: for v in app_iter: yield v finally: if hasattr(app_iter, "close"): app_iter.close() return
def check_write_permission(self, url, depth, token_list, principal): """Check, if <principal> can modify <url>, otherwise raise HTTP_LOCKED. If modifying <url> is prevented by a lock, DAVError(HTTP_LOCKED) is raised. An embedded DAVErrorCondition contains the conflicting locks. <url> may be modified by <principal>, if it is not currently locked directly or indirectly (i.e. by a locked parent). For depth-infinity operations, <url> also must not have locked children. It is not enough to check whether a lock is owned by <principal>, but also the token must be passed with the request. Because <principal> may run two different applications. See http://www.webdav.org/specs/rfc4918.html#lock-model http://www.webdav.org/specs/rfc4918.html#rfc.section.7.4 TODO: verify assumptions: - Parent locks WILL NOT be conflicting, if they are depth-0. - Exclusive child locks WILL be conflicting, even if they are owned by <principal>. @param url: URL that shall be modified, created, moved, or deleted @param depth: "0"|"infinity" @param token_list: list of lock tokens, that the principal submitted in If: header @param principal: name of the principal requesting a lock @return: None or raise error """ assert compat.is_native(url) assert depth in ("0", "infinity") _logger.debug( "check_write_permission({}, {}, {}, {})".format( url, depth, token_list, principal ) ) # Error precondition to collect conflicting URLs errcond = DAVErrorCondition(PRECONDITION_CODE_LockConflict) self._lock.acquire_read() try: # Check url and all parents for conflicting locks u = url while u: ll = self.get_url_lock_list(u) _logger.debug(" checking {}".format(u)) for l in ll: _logger.debug(" l={}".format(lock_string(l))) if u != url and l["depth"] != "infinity": # We only consider parents with Depth: inifinity continue elif principal == l["principal"] and l["token"] in token_list: # User owns this lock continue else: # Token is owned by principal, but not passed with lock list _logger.debug( " -> DENIED due to locked parent {}".format(lock_string(l)) ) errcond.add_href(l["root"]) u = util.get_uri_parent(u) if depth == "infinity": # Check child URLs for conflicting locks childLocks = self.storage.get_lock_list( url, include_root=False, include_children=True, token_only=False ) for l in childLocks: assert util.is_child_uri(url, l["root"]) # if util.is_child_uri(url, l["root"]): _logger.debug( " -> DENIED due to locked child {}".format(lock_string(l)) ) errcond.add_href(l["root"]) finally: self._lock.release() # If there were conflicts, raise HTTP_LOCKED for <url>, and pass # conflicting resource with 'no-conflicting-lock' precondition if len(errcond.hrefs) > 0: raise DAVError(HTTP_LOCKED, err_condition=errcond) return
def __call__(self, environ, start_response): # util.log("SCRIPT_NAME='{}', PATH_INFO='{}'".format( # environ.get("SCRIPT_NAME"), environ.get("PATH_INFO"))) path = environ["PATH_INFO"] # (#73) Failed on processing non-iso-8859-1 characters on Python 3 # # Note: we encode using UTF-8 here (falling back to ISO-8859-1)! # This seems to be wrong, since per PEP 3333 PATH_INFO is always ISO-8859-1 encoded # (see https://www.python.org/dev/peps/pep-3333/#unicode-issues). # But also seems to resolve errors when accessing resources with Chinese characters, for # example. # This is done by default for Python 3, but can be turned off in settings. if self.re_encode_path_info: path = environ["PATH_INFO"] = compat.wsgi_to_bytes(path).decode() # We optionally unquote PATH_INFO here, although this should already be # done by the server (#8). if self.unquote_path_info: path = compat.unquote(environ["PATH_INFO"]) # GC issue 22: Pylons sends root as u'/' if not compat.is_native(path): _logger.warning("Got non-native PATH_INFO: {!r}".format(path)) # path = path.encode("utf8") path = compat.to_native(path) # 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 share, provider = self.resolve_provider(path) # share = None # lower_path = path.lower() # for r in self.sorted_share_list: # # @@: 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 lower_path == r or lower_path.startswith(r + "/"): # share = r # break # 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' # if share is not None: # share_data = self.provider_map.get(share) # environ["wsgidav.provider"] = share_data["provider"] 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) :] # assert isinstance(path, str) assert compat.is_native(path) # 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: _logger.error("Duplicate header in response: {}".format(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 statusCode not in (204, 304) ) # _logger.info(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 _logger.error( "Missing required Content-Length header in {}-response: closing connection".format( statusCode ) ) forceCloseConnection = True elif not type(currentContentLength) is str: _logger.error( "Invalid Content-Length header in response ({!r}): closing connection".format( headerDict.get("content-length") ) ) forceCloseConnection = True # HOTFIX for Vista and Windows 7 (GC 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.read_and_discard_input(environ) # Make sure the socket is not reused, unless we are 100% sure all # current input was consumed if util.get_content_length(environ) != 0 and not environ.get( "wsgidav.all_input_read" ): _logger.warning( "Input stream not completely consumed: closing connection." ) forceCloseConnection = True if forceCloseConnection and headerDict.get("connection") != "close": _logger.warning("Adding 'Connection: close' header.") response_headers.append(("Connection", "close")) # Log request if self.verbose >= 3: userInfo = environ.get("wsgidav.auth.user_name") if not userInfo: userInfo = "(anonymous)" extra = [] if "HTTP_DESTINATION" in environ: extra.append('dest="{}"'.format(environ.get("HTTP_DESTINATION"))) if environ.get("CONTENT_LENGTH", "") != "": extra.append("length={}".format(environ.get("CONTENT_LENGTH"))) if "HTTP_DEPTH" in environ: extra.append("depth={}".format(environ.get("HTTP_DEPTH"))) if "HTTP_RANGE" in environ: extra.append("range={}".format(environ.get("HTTP_RANGE"))) if "HTTP_OVERWRITE" in environ: extra.append("overwrite={}".format(environ.get("HTTP_OVERWRITE"))) if self.verbose >= 3 and "HTTP_EXPECT" in environ: extra.append('expect="{}"'.format(environ.get("HTTP_EXPECT"))) if self.verbose >= 4 and "HTTP_CONNECTION" in environ: extra.append( 'connection="{}"'.format(environ.get("HTTP_CONNECTION")) ) if self.verbose >= 4 and "HTTP_USER_AGENT" in environ: extra.append('agent="{}"'.format(environ.get("HTTP_USER_AGENT"))) if self.verbose >= 4 and "HTTP_TRANSFER_ENCODING" in environ: extra.append( "transfer-enc={}".format(environ.get("HTTP_TRANSFER_ENCODING")) ) if self.verbose >= 3: extra.append("elap={:.3f}sec".format(time.time() - start_time)) extra = ", ".join(extra) # This is the CherryPy format: # 127.0.0.1 - - [08/Jul/2009:17:25:23] "GET /loginPrompt?redirect=/renderActionList%3Frelation%3Dpersonal%26key%3D%26filter%3DprivateSchedule&reason=0 HTTP/1.1" 200 1944 "http://127.0.0.1:8002/command?id=CMD_Schedule" "Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.1) Gecko/20090624 Firefox/3.5" # noqa _logger.info( '{addr} - {user} - [{time}] "{method} {path}" {extra} -> {status}'.format( addr=environ.get("REMOTE_ADDR", ""), user=userInfo, time=util.get_log_time(), method=environ.get("REQUEST_METHOD"), path=safe_re_encode( environ.get("PATH_INFO", ""), sys.stdout.encoding if sys.stdout.encoding else "utf-8", ), extra=extra, status=status, # response_headers.get(""), # response Content-Length # referer ) ) return start_response(status, response_headers, exc_info) # Call first middleware app_iter = self.application(environ, _start_response_wrapper) try: for v in app_iter: yield v finally: if hasattr(app_iter, "close"): app_iter.close() return
def __call__(self, environ, start_response): # util.log("SCRIPT_NAME='%s', PATH_INFO='%s'" % ( # environ.get("SCRIPT_NAME"), environ.get("PATH_INFO"))) # We optionall unquote PATH_INFO here, although this should already be # done by the server (#8). path = environ["PATH_INFO"] if self.config.get("unquote_path_info", False): path = compat.unquote(environ["PATH_INFO"]) # GC issue 22: Pylons sends root as u'/' # if isinstance(path, unicode): if not compat.is_native(path): util.log("Got non-native PATH_INFO: %r" % path) # path = path.encode("utf8") path = compat.to_native(path) # 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) shareList = sorted(self.providerMap.keys(), 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 # 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' if share is not None: share_data = self.providerMap.get(share) environ["wsgidav.provider"] = share_data['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) assert compat.is_native(path) # 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 (GC 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.currentThread().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) # This is the CherryPy format: # 127.0.0.1 - - [08/Jul/2009:17:25:23] "GET /loginPrompt?redirect=/renderActionList%3Frelation%3Dpersonal%26key%3D%26filter%3DprivateSchedule&reason=0 HTTP/1.1" 200 1944 "http://127.0.0.1:8002/command?id=CMD_Schedule" "Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.1) Gecko/20090624 Firefox/3.5" # print >>sys.stderr, '%s - %s - [%s] "%s" %s -> %s' % ( print( '%s - %s - [%s] "%s" %s -> %s' % ( threadInfo + environ.get("REMOTE_ADDR", ""), userInfo, util.getLogTime(), environ.get("REQUEST_METHOD") + " " + environ.get("PATH_INFO", ""), extra, status, # response_headers.get(""), # response Content-Length # referer ), file=sys.stdout) return start_response(status, response_headers, exc_info) # Call next middleware app_iter = self._application(environ, _start_response_wrapper) for v in app_iter: yield v if hasattr(app_iter, "close"): app_iter.close() return