package dav import ( "context" "encoding/xml" "fmt" "io" "net/http" "net/url" "strings" ) // FindPrincipal requires authenticated credentials on client. func FindPrincipal(ctx context.Context, client *http.Client, contextURL string) (string, error) { body := ` ` hrefs, err := propFind(ctx, client, contextURL, "0", body) if err != nil { return "", err } for _, href := range hrefs.principalHref() { return resolveReference(contextURL, href), nil } return "", fmt.Errorf("no current-user-principal returned") } func FindScheduleURLs(ctx context.Context, client *http.Client, principalURL string) (inbox, outbox string, err error) { body := ` ` resp, err := propFind(ctx, client, principalURL, "0", body) if err != nil { return "", "", err } for _, r := range resp.Response { for _, ps := range r.Propstat { if !strings.Contains(ps.Status, "200") { continue } for _, p := range ps.Prop.Props { switch p.XMLName.Local { case "schedule-inbox-URL": if h := p.firstHref(); h != "" { inbox = resolveReference(principalURL, h) } case "schedule-outbox-URL": if h := p.firstHref(); h != "" { outbox = resolveReference(principalURL, h) } } } } } return inbox, outbox, nil } // multistatus is intentionally a permissive subset: unknown elements are // ignored so server-specific extensions don't break parsing. type multistatus struct { XMLName xml.Name `xml:"DAV: multistatus"` Response []msResponse `xml:"response"` } type msResponse struct { Href string `xml:"href"` Propstat []propstat `xml:"propstat"` } type propstat struct { Prop prop `xml:"prop"` Status string `xml:"status"` } type prop struct { Props []msProp `xml:",any"` } type msProp struct { XMLName xml.Name Hrefs []string `xml:"href"` } func (p msProp) firstHref() string { if len(p.Hrefs) > 0 { return p.Hrefs[0] } return "" } func (m *multistatus) principalHref() []string { var out []string for _, r := range m.Response { for _, ps := range r.Propstat { if !strings.Contains(ps.Status, "200") { continue } for _, pr := range ps.Prop.Props { if pr.XMLName.Local == "current-user-principal" { if h := pr.firstHref(); h != "" { out = append(out, h) } } } } } return out } // propFind is tuned for small single-resource probes; not for large listings. func propFind(ctx context.Context, client *http.Client, url, depth, body string) (*multistatus, error) { req, err := http.NewRequestWithContext(ctx, "PROPFIND", url, strings.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/xml; charset=utf-8") req.Header.Set("Depth", depth) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() // 10 MiB cap: probes here read a handful of props on one resource; more // is either misbehaviour or an attempt at memory exhaustion. data, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20)) if err != nil { return nil, err } if resp.StatusCode != http.StatusMultiStatus && resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("PROPFIND returned HTTP %d", resp.StatusCode) } var ms multistatus if err := xml.Unmarshal(data, &ms); err != nil { return nil, fmt.Errorf("invalid multistatus: %w", err) } return &ms, nil } func resolveReference(base, ref string) string { r, err := url.Parse(ref) if err != nil { return ref } b, err := url.Parse(base) if err != nil { return ref } return b.ResolveReference(r).String() }