Initial commit
CalDAV and CardDAV checkers sharing a single Go module. Discovery follows RFC 6764 (/.well-known + SRV/TXT), authenticated probes cover principal, home-set, collections and a minimal REPORT query on top of go-webdav. Common shape in internal/dav/; CalDAV adds a scheduling rule. Surfaces its context URL (and each secure-SRV target) as TLS endpoints via the EndpointDiscoverer interface, so the dedicated TLS checker can pick them up without re-parsing observations. HTML report foregrounds common misconfigs (well-known returning 200, missing SRV, plaintext-only SRV, missing DAV capability, skipped auth phase) as action-item callouts before the full phase breakdown.
This commit is contained in:
commit
aae1452e12
37 changed files with 2730 additions and 0 deletions
161
internal/dav/principal.go
Normal file
161
internal/dav/principal.go
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
package dav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FindPrincipal issues a PROPFIND for {DAV:}current-user-principal against
|
||||
// contextURL. Callers should attach credentials to client; a 401/403 bubbles
|
||||
// up as the returned error.
|
||||
func FindPrincipal(ctx context.Context, client *http.Client, contextURL string) (string, error) {
|
||||
body := `<?xml version="1.0" encoding="utf-8"?>
|
||||
<d:propfind xmlns:d="DAV:">
|
||||
<d:prop><d:current-user-principal/></d:prop>
|
||||
</d:propfind>`
|
||||
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")
|
||||
}
|
||||
|
||||
// FindScheduleURLs looks up {urn:ietf:params:xml:ns:caldav}schedule-inbox-URL
|
||||
// and schedule-outbox-URL on the CalDAV principal URL. CalDAV-only.
|
||||
func FindScheduleURLs(ctx context.Context, client *http.Client, principalURL string) (inbox, outbox string, err error) {
|
||||
body := `<?xml version="1.0" encoding="utf-8"?>
|
||||
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||
<d:prop>
|
||||
<c:schedule-inbox-URL/>
|
||||
<c:schedule-outbox-URL/>
|
||||
</d:prop>
|
||||
</d:propfind>`
|
||||
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
|
||||
}
|
||||
|
||||
// ── raw PROPFIND ─────────────────────────────────────────────────────────────
|
||||
|
||||
// multistatus is the subset of the DAV:multistatus XML schema we need to read
|
||||
// principal URLs and scheduling hrefs. It is intentionally permissive — extra
|
||||
// elements are ignored, which makes us tolerant of server-specific extensions.
|
||||
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"`
|
||||
// A prop may also contain nested <current-user-principal><href>…</href></current-user-principal>,
|
||||
// which the flat Hrefs slice above captures via xml:"href" descent.
|
||||
}
|
||||
|
||||
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 a small PROPFIND helper tuned for small single-resource probes.
|
||||
// It returns a parsed multistatus; transport-level failures bubble up as err.
|
||||
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()
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
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 := parseURL(ref)
|
||||
if err != nil {
|
||||
return ref
|
||||
}
|
||||
b, err := parseURL(base)
|
||||
if err != nil {
|
||||
return ref
|
||||
}
|
||||
return b.ResolveReference(r).String()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue