Esempio n. 1
0
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"])
Esempio n. 2
0
    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
Esempio n. 3
0
    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()
Esempio n. 4
0
 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
     )
Esempio n. 5
0
    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
Esempio n. 6
0
    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
Esempio n. 7
0
    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
Esempio n. 8
0
    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
Esempio n. 9
0
    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
Esempio n. 10
0
    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
Esempio n. 11
0
    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
Esempio n. 12
0
    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