package internal import ( "bytes" "encoding/xml" "fmt" "io" "mime" "net/http" "net/url" "path" "strings" "unicode" ) // HTTPClient performs HTTP requests. It's implemented by *http.Client. type HTTPClient interface { Do(req *http.Request) (*http.Response, error) } type Client struct { http HTTPClient endpoint *url.URL } func NewClient(c HTTPClient, endpoint string) (*Client, error) { if c == nil { c = http.DefaultClient } u, err := url.Parse(endpoint) if err != nil { return nil, err } if u.Path == "" { // This is important to avoid issues with path.Join u.Path = "/" } return &Client{http: c, endpoint: u}, nil } func (c *Client) ResolveHref(p string) *url.URL { if !strings.HasPrefix(p, "/") { p = path.Join(c.endpoint.Path, p) } return &url.URL{ Scheme: c.endpoint.Scheme, User: c.endpoint.User, Host: c.endpoint.Host, Path: p, } } func (c *Client) NewRequest(method string, path string, body io.Reader) (*http.Request, error) { return http.NewRequest(method, c.ResolveHref(path).String(), body) } func (c *Client) NewXMLRequest(method string, path string, v interface{}) (*http.Request, error) { var buf bytes.Buffer buf.WriteString(xml.Header) if err := xml.NewEncoder(&buf).Encode(v); err != nil { return nil, err } req, err := c.NewRequest(method, path, &buf) if err != nil { return nil, err } req.Header.Add("Content-Type", "text/xml; charset=\"utf-8\"") return req, nil } func (c *Client) Do(req *http.Request) (*http.Response, error) { resp, err := c.http.Do(req) if err != nil { return nil, err } if resp.StatusCode/100 != 2 { defer resp.Body.Close() contentType := resp.Header.Get("Content-Type") if contentType == "" { contentType = "text/plain" } var wrappedErr error t, _, _ := mime.ParseMediaType(contentType) if t == "application/xml" || t == "text/xml" { var davErr Error if err := xml.NewDecoder(resp.Body).Decode(&davErr); err != nil { wrappedErr = err } else { wrappedErr = &davErr } } else if strings.HasPrefix(t, "text/") { lr := io.LimitedReader{R: resp.Body, N: 1024} var buf bytes.Buffer io.Copy(&buf, &lr) resp.Body.Close() if s := strings.TrimSpace(buf.String()); s != "" { if lr.N == 0 { s += " […]" } wrappedErr = fmt.Errorf("%v", s) } } return nil, &HTTPError{Code: resp.StatusCode, Err: wrappedErr} } return resp, nil } func (c *Client) DoMultiStatus(req *http.Request) (*MultiStatus, error) { resp, err := c.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusMultiStatus { return nil, fmt.Errorf("HTTP multi-status request failed: %v", resp.Status) } // TODO: the response can be quite large, support streaming Response elements var ms MultiStatus if err := xml.NewDecoder(resp.Body).Decode(&ms); err != nil { return nil, err } return &ms, nil } func (c *Client) PropFind(path string, depth Depth, propfind *PropFind) (*MultiStatus, error) { req, err := c.NewXMLRequest("PROPFIND", path, propfind) if err != nil { return nil, err } req.Header.Add("Depth", depth.String()) return c.DoMultiStatus(req) } // PropfindFlat performs a PROPFIND request with a zero depth. func (c *Client) PropFindFlat(path string, propfind *PropFind) (*Response, error) { ms, err := c.PropFind(path, DepthZero, propfind) if err != nil { return nil, err } // If the client followed a redirect, the Href might be different from the request path if len(ms.Responses) != 1 { return nil, fmt.Errorf("PROPFIND with Depth: 0 returned %d responses", len(ms.Responses)) } return &ms.Responses[0], nil } func parseCommaSeparatedSet(values []string, upper bool) map[string]bool { m := make(map[string]bool) for _, v := range values { fields := strings.FieldsFunc(v, func(r rune) bool { return unicode.IsSpace(r) || r == ',' }) for _, f := range fields { if upper { f = strings.ToUpper(f) } else { f = strings.ToLower(f) } m[f] = true } } return m } func (c *Client) Options(path string) (classes map[string]bool, methods map[string]bool, err error) { req, err := c.NewRequest(http.MethodOptions, path, nil) if err != nil { return nil, nil, err } resp, err := c.Do(req) if err != nil { return nil, nil, err } resp.Body.Close() classes = parseCommaSeparatedSet(resp.Header["Dav"], false) if !classes["1"] { return nil, nil, fmt.Errorf("webdav: server doesn't support DAV class 1") } methods = parseCommaSeparatedSet(resp.Header["Allow"], true) return classes, methods, nil } // SyncCollection perform a `sync-collection` REPORT operation on a resource func (c *Client) SyncCollection(path, syncToken string, level Depth, limit *Limit, prop *Prop) (*MultiStatus, error) { q := SyncCollectionQuery{ SyncToken: syncToken, SyncLevel: level.String(), Limit: limit, Prop: prop, } req, err := c.NewXMLRequest("REPORT", path, &q) if err != nil { return nil, err } ms, err := c.DoMultiStatus(req) if err != nil { return nil, err } return ms, nil }