package caldav 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/caldav" ) // Collect is intentionally resilient: each phase records its outcome and we // keep going as long as the next phase has something to work with. Rules // later turn the captured state into CheckStates. func (p *caldavProvider) 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.KindCalDAV, Domain: domain, HasCredentials: user != "" && pass != "", CollectedAt: time.Now(), Scheduling: &dav.SchedulingResult{}, } anonClient := dav.NewHTTPClient(timeout) // Phase 1: Discovery obs.Discovery = dav.Discover(ctx, anonClient, dav.KindCalDAV, domain, explicit) if obs.Discovery.ContextURL == "" { return obs, nil } // Phase 2: Transport + OPTIONS (no auth required) 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} obs.Scheduling.Advertised = optsRes.HasCapability("calendar-schedule") // Phase 3: Authenticated probes 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 cal, err := caldav.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 := cal.FindCalendarHomeSet(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 calendars, err := cal.FindCalendars(ctx, home) if err != nil { obs.Collections.Error = err.Error() obs.Report.Skipped = true } else { for _, c := range calendars { obs.Collections.Items = append(obs.Collections.Items, dav.CollectionInfo{ Path: c.Path, Name: c.Name, Description: c.Description, MaxResourceSize: c.MaxResourceSize, SupportedComponentSet: c.SupportedComponentSet, }) } } // Empty calendar-query against the first calendar: cheapest probe that // still exercises the REPORT pipeline end-to-end. if len(obs.Collections.Items) > 0 { first := obs.Collections.Items[0].Path obs.Report.ProbePath = first q := &caldav.CalendarQuery{ CompRequest: caldav.CalendarCompRequest{ Name: "VCALENDAR", Comps: []caldav.CalendarCompRequest{ {Name: "VEVENT"}, }, }, } if _, err := cal.QueryCalendar(ctx, first, q); err != nil { obs.Report.Error = err.Error() } else { obs.Report.QueryOK = true } } else { obs.Report.Skipped = true } if obs.Scheduling.Advertised { inbox, outbox, err := dav.FindScheduleURLs(ctx, authClient, principal) if err != nil { obs.Scheduling.Error = err.Error() } obs.Scheduling.InboxURL = inbox obs.Scheduling.OutboxURL = outbox } return obs, nil } func asHTTPClient(c *http.Client) webdav.HTTPClient { return c }