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.
125 lines
3.3 KiB
Go
125 lines
3.3 KiB
Go
package carddav
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
"git.happydns.org/checker-dav/internal/dav"
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
webdav "github.com/emersion/go-webdav"
|
|
"github.com/emersion/go-webdav/carddav"
|
|
)
|
|
|
|
func (p *carddavProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) {
|
|
domain, _ := sdk.GetOption[string](opts, "domain_name")
|
|
user, _ := sdk.GetOption[string](opts, "username")
|
|
pass, _ := sdk.GetOption[string](opts, "password")
|
|
explicit, _ := sdk.GetOption[string](opts, "context_url")
|
|
timeoutSec := sdk.GetFloatOption(opts, "timeout_seconds", 10)
|
|
|
|
timeout := time.Duration(timeoutSec * float64(time.Second))
|
|
if timeout <= 0 {
|
|
timeout = 10 * time.Second
|
|
}
|
|
|
|
obs := &dav.Observation{
|
|
Kind: dav.KindCardDAV,
|
|
Domain: domain,
|
|
HasCredentials: user != "" && pass != "",
|
|
CollectedAt: time.Now(),
|
|
}
|
|
|
|
anonClient := dav.NewHTTPClient(timeout)
|
|
|
|
// Phase 1: Discovery
|
|
obs.Discovery = dav.Discover(ctx, anonClient, dav.KindCardDAV, domain, explicit)
|
|
if obs.Discovery.ContextURL == "" {
|
|
return obs, nil
|
|
}
|
|
|
|
// Phase 2: OPTIONS
|
|
optsRes, err := dav.ProbeOptions(ctx, anonClient, obs.Discovery.ContextURL)
|
|
obs.Options = optsRes
|
|
if err != nil {
|
|
obs.Transport = dav.TransportResult{Error: err.Error()}
|
|
return obs, nil
|
|
}
|
|
obs.Transport = dav.TransportResult{Reached: true}
|
|
|
|
// Phase 3: Authenticated
|
|
if !obs.HasCredentials {
|
|
obs.Principal.Skipped = true
|
|
obs.HomeSet.Skipped = true
|
|
obs.Collections.Skipped = true
|
|
obs.Report.Skipped = true
|
|
return obs, nil
|
|
}
|
|
|
|
authClient := dav.WithBasicAuth(anonClient, obs.Discovery.ContextURL, user, pass)
|
|
|
|
principal, err := dav.FindPrincipal(ctx, authClient, obs.Discovery.ContextURL)
|
|
if err != nil {
|
|
obs.Principal.Error = err.Error()
|
|
obs.HomeSet.Skipped = true
|
|
obs.Collections.Skipped = true
|
|
obs.Report.Skipped = true
|
|
return obs, nil
|
|
}
|
|
obs.Principal.URL = principal
|
|
|
|
card, err := carddav.NewClient(asHTTPClient(authClient), obs.Discovery.ContextURL)
|
|
if err != nil {
|
|
obs.HomeSet.Error = err.Error()
|
|
obs.Collections.Skipped = true
|
|
obs.Report.Skipped = true
|
|
return obs, nil
|
|
}
|
|
home, err := card.FindAddressBookHomeSet(ctx, principal)
|
|
if err != nil {
|
|
obs.HomeSet.Error = err.Error()
|
|
obs.Collections.Skipped = true
|
|
obs.Report.Skipped = true
|
|
return obs, nil
|
|
}
|
|
obs.HomeSet.URL = home
|
|
|
|
books, err := card.FindAddressBooks(ctx, home)
|
|
if err != nil {
|
|
obs.Collections.Error = err.Error()
|
|
obs.Report.Skipped = true
|
|
} else {
|
|
for _, b := range books {
|
|
item := dav.CollectionInfo{
|
|
Path: b.Path,
|
|
Name: b.Name,
|
|
Description: b.Description,
|
|
MaxResourceSize: b.MaxResourceSize,
|
|
}
|
|
for _, d := range b.SupportedAddressData {
|
|
item.SupportedAddressData = append(item.SupportedAddressData, d.ContentType+";"+d.Version)
|
|
}
|
|
obs.Collections.Items = append(obs.Collections.Items, item)
|
|
}
|
|
}
|
|
|
|
if len(obs.Collections.Items) > 0 {
|
|
first := obs.Collections.Items[0].Path
|
|
obs.Report.ProbePath = first
|
|
q := &carddav.AddressBookQuery{
|
|
DataRequest: carddav.AddressDataRequest{AllProp: true},
|
|
Limit: 1,
|
|
}
|
|
if _, err := card.QueryAddressBook(ctx, first, q); err != nil {
|
|
obs.Report.Error = err.Error()
|
|
} else {
|
|
obs.Report.QueryOK = true
|
|
}
|
|
} else {
|
|
obs.Report.Skipped = true
|
|
}
|
|
|
|
return obs, nil
|
|
}
|
|
|
|
func asHTTPClient(c *http.Client) webdav.HTTPClient { return c }
|