emersion / go-webdav

A Go library for WebDAV, CalDAV and CardDAV
MIT License
314 stars 66 forks source link

[Solution included] This client isn't compatible with the Hetzner storage box WebDAV server #123

Open oddmario opened 1 year ago

oddmario commented 1 year ago

Hello,

I tried using your WebDAV client to access my Hetzner storage box through their WebDAV server (pretty sure they use Apache as their WebDAV server)

First off, I noticed how their WebDAV differs than most of the WebDAV servers I have used before:

So what's the issue?

If we perform a Readdir (for example) query on the Hetzner WebDAV server like that:

data, err := webdav.Readdir("/asset1", false)
if err != nil {
    fmt.Println(err)
}

the internal HTTP client of go-webdav will send the request with the URL https://uXXXX.your-storagebox.de/asset1 and the method PROPFIND, but then the server will respond with HTTP status code 302 and a Location: https://uXXXX.your-storagebox.de/asset1/ header. This really shouldn't be an issue since the Golang HTTP client follows any redirects by default (I actually couldn't debug what I have just stated without disabling the automatic redirections following first). However the request made after the redirect loses its request method (PROPFIND) and always gets changed to a GET request, and here all the issues start (since as I said in the start, the Hetzner WebDAV server will respond with HTML if the request method isn't one of the WebDAV request verbs)

I don't really know why the HTTP client doesn't follow the redirections with the same request method of the original request, but I managed to solve it by creating my own Transport like that:

type webdavTransport struct {
    username string
    password string
}

func (t *webdavTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    req.SetBasicAuth(t.username, t.password)

    var resp *http.Response
    var err error

    resp, err = http.DefaultTransport.RoundTrip(req)

    if err == nil {
        if resp.StatusCode == 301 || resp.StatusCode == 302 {
            newURL, _ := resp.Location()

            reqNew, _ := http.NewRequest(req.Method, newURL.String(), req.Body)
            reqNew.Header = req.Header

            respNew, errNew := http.DefaultTransport.RoundTrip(reqNew)

            resp = respNew
            err = errNew
        }
    }

    return resp, err
}

func FixedWebdavClientWithAuth(url, username, password string) (*webdav.Client, error) {
    client := &http.Client{
        Transport: &webdavTransport{
            username: username,
            password: password,
        },
                // Disable any redirections following since we'll manually handle this in the transport's RoundTripper
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            return http.ErrUseLastResponse
        },
    }

    return webdav.NewClient(client, url)
}

now using

webdav, err := FixedWebdavClientWithAuth("https://uXXXX.your-storagebox.de", "[redacted]", "[redacted]")

if err != nil {
    fmt.Println("unable to access")
}

data, err := webdav.Readdir("/asset1", false)
if err != nil {
    fmt.Println(err)
    fmt.Println("unable to access")
}

fmt.Println(data)

solves the issue and data gets printed successfully with no issues

emersion commented 1 year ago

See https://cs.opensource.google/go/go/+/refs/tags/go1.21.0:src/net/http/client.go;l=518;drc=97df9c0b051b6c06bff7ec7e61522e7bbef40c91

jech commented 9 months ago

In other words, the server should be replying with 307 instead of 302.