checker-dav/internal/dav/options.go
Pierre-Olivier Mercier 7d5535fddf 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.
2026-04-26 21:47:40 +07:00

94 lines
2.1 KiB
Go

package dav
import (
"context"
"fmt"
"net/http"
"strings"
)
// ProbeOptions never treats a missing/incomplete DAV: header as a transport
// error: severity is the caller rule's decision, not ours.
func ProbeOptions(ctx context.Context, client *http.Client, url string) (OptionsResult, error) {
res := OptionsResult{}
req, err := http.NewRequestWithContext(ctx, http.MethodOptions, url, nil)
if err != nil {
res.Error = err.Error()
return res, err
}
resp, err := client.Do(req)
if err != nil {
res.Error = err.Error()
return res, err
}
defer resp.Body.Close()
res.StatusCode = resp.StatusCode
res.Server = resp.Header.Get("Server")
res.DAVClasses = parseCSVHeader(resp.Header.Values("Dav"))
res.AllowMethods = parseCSVHeader(resp.Header.Values("Allow"))
for _, h := range resp.Header.Values("Www-Authenticate") {
if scheme := authScheme(h); scheme != "" {
res.AuthSchemes = appendUnique(res.AuthSchemes, scheme)
}
}
if res.StatusCode >= 400 {
res.Error = fmt.Sprintf("OPTIONS returned HTTP %d", res.StatusCode)
}
return res, nil
}
// HasCapability matches case-insensitively per RFC 4918 §10.1.
func (o OptionsResult) HasCapability(cap string) bool {
for _, c := range o.DAVClasses {
if strings.EqualFold(c, cap) {
return true
}
}
return false
}
func (o OptionsResult) AllowsMethod(m string) bool {
for _, a := range o.AllowMethods {
if strings.EqualFold(a, m) {
return true
}
}
return false
}
// parseCSVHeader merges repeated headers (net/http keeps them separate)
// into a single split-and-trimmed slice.
func parseCSVHeader(values []string) []string {
var out []string
for _, v := range values {
for _, part := range strings.Split(v, ",") {
if p := strings.TrimSpace(part); p != "" {
out = append(out, p)
}
}
}
return out
}
func authScheme(h string) string {
h = strings.TrimSpace(h)
if h == "" {
return ""
}
if i := strings.IndexAny(h, " \t"); i > 0 {
return h[:i]
}
return h
}
func appendUnique(s []string, v string) []string {
for _, x := range s {
if strings.EqualFold(x, v) {
return s
}
}
return append(s, v)
}