skshetry / webdav4

WebDAV client library with a fsspec based filesystem and a CLI.
https://skshetry.github.io/webdav4
MIT License
61 stars 17 forks source link

Error in get_props() with lighttpd #178

Open RomainTT opened 1 month ago

RomainTT commented 1 month ago

Observations

When using lighttpd webdav module as a webdav server. When using get_props on a resource (directory or file). The following exception is raised:

    223 def get_response_for_path(self, hostname: str, path: str) -> Response:
    224     """Provides response for the resource with the specific href/path.
    225
    226     Args:
   (...)
    231             resource.
    232     """
--> 233     return self.responses[join_url_path(hostname, path)]

KeyError: 'path/to/my/file'

This is because propfind returns an empty content (the XML is valid, but with no data).

In[0]: client.propfind("./").content
Out[0]: '<?xml version="1.0" encoding="utf-8"?>\n<D:multistatus xmlns:D="DAV:" xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">\n</D:multistatus>\n'

Solution

After several tests I found out that propfind works well if the correct Depth header.

In[0]: client.propfind("path/to/file", headers={"Depth": "0"}).content
Out[0]: '<?xml version="1.0" encoding="utf-8"?>\n<D:multistatus xmlns:D="DAV:" xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">\n<D:response>\n<D:href>http://webdav-url/path/to/file</D:href>\n<D:propstat>\n<D:prop>\n<D:creationdate ns0:dt="dateTime.tz">2024-07-11T09:33:29Z</D:creationdate><D:getcontentlanguage>en</D:getcontentlanguage><D:getcontentlength>508</D:getcontentlength><D:getlastmodified ns0:dt="dateTime.rfc1123">Thu, 11 Jul 2024 09:33:29 GMT</D:getlastmodified></D:prop>\n<D:status>HTTP/1.1 200 OK</D:status>\n</D:propstat>\n</D:response>\n</D:multistatus>\n'                                  

As get_props is meant to read properties of a single filepath, it seems relevant to add Depth: 0 to the headers in this method.

Here is a fix which is working for me:

    def get_props(
        self,
        path: str,
        name: Optional[str] = None,
        namespace: Optional[str] = None,
        data: Optional[str] = None,
    ) -> "DAVProperties":
        """Returns properties of a resource by doing a propfind request.

        Can also selectively request the properties by passing name or data.
        """
        data = data or prepare_propfind_request_data(name, namespace)
        headers = {"Content-Type": "application/xml"} if data else {}
        headers["Depth"] = "0"  # <-- This line enforce the Depth header
        result = self.propfind(path, headers=headers, data=data)
        response = result.get_response_for_path(self.base_url.path, path)
        return response.properties

But I am not sure of the all the consequences, maybe I’m missing the big picture. I did not test this with another webdav server like Nextcloud.

What do you think?

gstrauss commented 1 month ago

The WebDAV RFC was published June 2007 (which is 17 years ago) https://www.rfc-editor.org/rfc/rfc4918#section-9.1

A client MUST submit a Depth header with a value of "0", "1", or "infinity" with a PROPFIND request. Servers MUST support "0" and "1" depth requests on WebDAV-compliant resources and SHOULD support "infinity" requests. In practice, support for infinite-depth requests MAY be disabled, due to the performance and security concerns associated with this behavior. Servers SHOULD treat a request without a Depth header as if a "Depth: infinity" header was included.

get_props() should most definitely be updated to send an appropriate Depth header.


Note: not all servers support Depth: infinity.

lighttpd supports Depth: infinity, but disables PROPFIND Depth: infinity by default, unless explicitly configured in lighttpd.conf: webdav.opts += ("propfind-depth-infinity" => "enable")

Therefore, if you send Depth: infinity, you should be prepared to fall back to multiple PROPFIND requests with Depth: 1