diff --git a/README.md b/README.md index a9c67bb..3ebfc8e 100644 --- a/README.md +++ b/README.md @@ -38,27 +38,27 @@ view; the default is a JSON metrics dump. Both checkers accept the same options: -- `domain_name` (auto-filled) — required -- `username`, `password` — optional Basic credentials; unlock authenticated +- `domain_name` (auto-filled): required +- `username`, `password`: optional Basic credentials; unlock authenticated checks (principal, home-set, collections, REPORT probe) -- `context_url` — optional explicit override, bypasses `/.well-known` + SRV -- `timeout_seconds` — per-request HTTP timeout, default 10 +- `context_url`: optional explicit override, bypasses `/.well-known` + SRV +- `timeout_seconds`: per-request HTTP timeout, default 10 ## What is checked -1. **Discovery** — `/.well-known/{caldav,carddav}` (must 3xx, not 200), +1. **Discovery**: `/.well-known/{caldav,carddav}` (must 3xx, not 200), `_caldavs._tcp` / `_carddavs._tcp` SRV, TXT `path=` hint. -2. **Transport** — HTTPS reachable. TLS certificate validation is - deliberately out of scope — a dedicated TLS checker covers that. -3. **OPTIONS** — `DAV:` advertises `calendar-access` or `addressbook`; Allow +2. **Transport**: HTTPS reachable. TLS certificate validation is + deliberately out of scope; a dedicated TLS checker covers that. +3. **OPTIONS**: `DAV:` advertises `calendar-access` or `addressbook`; Allow includes `PROPFIND` and `REPORT`; auth schemes captured for info. -4. **Principal** — PROPFIND `current-user-principal` (auth required). -5. **Home-set** — `calendar-home-set` / `addressbook-home-set`. -6. **Collections** — enumerate, record properties (`supported-calendar-component-set`, +4. **Principal**: PROPFIND `current-user-principal` (auth required). +5. **Home-set**: `calendar-home-set` / `addressbook-home-set`. +6. **Collections**: enumerate, record properties (`supported-calendar-component-set`, `supported-address-data`, display name, description, max size). -7. **REPORT probe** — issue a minimal `calendar-query` / `addressbook-query` +7. **REPORT probe**: issue a minimal `calendar-query` / `addressbook-query` against the first collection. -8. **Scheduling** (CalDAV only) — if `calendar-schedule` is advertised, +8. **Scheduling** (CalDAV only): if `calendar-schedule` is advertised, verify `schedule-inbox-URL` and `schedule-outbox-URL` on the principal. The HTML report surfaces the most common failures at the top as callouts: @@ -71,5 +71,5 @@ The HTML report surfaces the most common failures at the top as callouts: ## Dependencies -- [`github.com/emersion/go-webdav`](https://github.com/emersion/go-webdav) — CalDAV/CardDAV client -- [`git.happydns.org/checker-sdk-go`](https://git.happydns.org/happyDomain/checker-sdk-go) — checker SDK +- [`github.com/emersion/go-webdav`](https://github.com/emersion/go-webdav): CalDAV/CardDAV client +- [`git.happydns.org/checker-sdk-go`](https://git.happydns.org/happyDomain/checker-sdk-go): checker SDK diff --git a/caldav/collect.go b/caldav/collect.go index 04dfc83..a73a977 100644 --- a/caldav/collect.go +++ b/caldav/collect.go @@ -38,13 +38,13 @@ func (p *caldavProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) ( anonClient := dav.NewHTTPClient(timeout) - // Phase 1 — Discovery + // 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) + // Phase 2: Transport + OPTIONS (no auth required) optsRes, err := dav.ProbeOptions(ctx, anonClient, obs.Discovery.ContextURL) obs.Options = optsRes if err != nil { @@ -54,7 +54,7 @@ func (p *caldavProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) ( obs.Transport = dav.TransportResult{Reached: true} obs.Scheduling.Advertised = optsRes.HasCapability("calendar-schedule") - // Phase 3 — Authenticated probes + // Phase 3: Authenticated probes if !obs.HasCredentials { obs.Principal.Skipped = true obs.HomeSet.Skipped = true @@ -110,7 +110,7 @@ func (p *caldavProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) ( } } - // Report probe — empty calendar-query against the first calendar. + // Report probe: empty calendar-query against the first calendar. if len(obs.Collections.Items) > 0 { first := obs.Collections.Items[0].Path obs.Report.ProbePath = first @@ -131,7 +131,7 @@ func (p *caldavProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) ( obs.Report.Skipped = true } - // Scheduling inbox/outbox — only probe if advertised. + // Scheduling inbox/outbox: only probe if advertised. if obs.Scheduling.Advertised { inbox, outbox, err := dav.FindScheduleURLs(ctx, authClient, principal) if err != nil { diff --git a/caldav/definition.go b/caldav/definition.go index 5899293..25af0bc 100644 --- a/caldav/definition.go +++ b/caldav/definition.go @@ -23,8 +23,8 @@ func Definition() *sdk.CheckerDefinition { // always offered at domain scope. ApplyToDomain: true, - // Also offered at service scope so alerts — including the TLS - // alerts derived from the endpoints we publish — surface on a + // Also offered at service scope so alerts, including the TLS + // alerts derived from the endpoints we publish, surface on a // dedicated "CalDAV" service page rather than on the domain // page. The abstract.CalDAV service type does not exist in the // happyDomain service catalog yet; until it does, this has no diff --git a/caldav/report.go b/caldav/report.go index 7355c2d..590f13d 100644 --- a/caldav/report.go +++ b/caldav/report.go @@ -17,7 +17,7 @@ import ( // // Downstream TLS probes published for the endpoints we discovered are read // via ctx.Related(dav.TLSRelatedKey) and folded into the report (callouts + -// dedicated TLS phase) — per +// dedicated TLS phase), per // happydomain3/docs/checker-discovery-endpoint.md. func (p *caldavProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error) { var d dav.Observation diff --git a/carddav/collect.go b/carddav/collect.go index a36ed57..d9172c7 100644 --- a/carddav/collect.go +++ b/carddav/collect.go @@ -32,13 +32,13 @@ func (p *carddavProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) anonClient := dav.NewHTTPClient(timeout) - // Phase 1 — Discovery + // Phase 1: Discovery obs.Discovery = dav.Discover(ctx, anonClient, dav.KindCardDAV, domain, explicit) if obs.Discovery.ContextURL == "" { return obs, nil } - // Phase 2 — OPTIONS + // Phase 2: OPTIONS optsRes, err := dav.ProbeOptions(ctx, anonClient, obs.Discovery.ContextURL) obs.Options = optsRes if err != nil { @@ -47,7 +47,7 @@ func (p *carddavProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) } obs.Transport = dav.TransportResult{Reached: true} - // Phase 3 — Authenticated + // Phase 3: Authenticated if !obs.HasCredentials { obs.Principal.Skipped = true obs.HomeSet.Skipped = true diff --git a/carddav/discovery.go b/carddav/discovery.go index dcb91ce..f975b7f 100644 --- a/carddav/discovery.go +++ b/carddav/discovery.go @@ -8,7 +8,7 @@ import ( ) // DiscoverEntries implements sdk.DiscoveryPublisher. See the CalDAV sibling -// for the rationale — the shared helper produces the TLS discovery entries. +// for the rationale; the shared helper produces the TLS discovery entries. func (p *carddavProvider) DiscoverEntries(data any) ([]sdk.DiscoveryEntry, error) { obs, ok := data.(*dav.Observation) if !ok { diff --git a/carddav/report.go b/carddav/report.go index 33610b7..60ae3b8 100644 --- a/carddav/report.go +++ b/carddav/report.go @@ -10,7 +10,7 @@ import ( ) // GetHTMLReport folds downstream TLS probes (published on our discovered -// endpoints) into the CardDAV report via ctx.Related — see the CalDAV +// endpoints) into the CardDAV report via ctx.Related; see the CalDAV // sibling for the rationale. func (p *carddavProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error) { var d dav.Observation diff --git a/cmd/checker-caldav/main.go b/cmd/checker-caldav/main.go index 78c1f00..dc9ae7d 100644 --- a/cmd/checker-caldav/main.go +++ b/cmd/checker-caldav/main.go @@ -5,7 +5,7 @@ import ( "log" "git.happydns.org/checker-dav/caldav" - sdk "git.happydns.org/checker-sdk-go/checker" + "git.happydns.org/checker-sdk-go/checker/server" ) // Version is injected at link time via -ldflags "-X main.Version=...". @@ -16,8 +16,8 @@ var listenAddr = flag.String("listen", ":8080", "HTTP listen address") func main() { flag.Parse() caldav.Version = Version - server := sdk.NewServer(caldav.Provider()) - if err := server.ListenAndServe(*listenAddr); err != nil { + srv := server.New(caldav.Provider()) + if err := srv.ListenAndServe(*listenAddr); err != nil { log.Fatalf("server error: %v", err) } } diff --git a/cmd/checker-carddav/main.go b/cmd/checker-carddav/main.go index 19f4c2c..721e84b 100644 --- a/cmd/checker-carddav/main.go +++ b/cmd/checker-carddav/main.go @@ -5,7 +5,7 @@ import ( "log" "git.happydns.org/checker-dav/carddav" - sdk "git.happydns.org/checker-sdk-go/checker" + "git.happydns.org/checker-sdk-go/checker/server" ) // Version is injected at link time via -ldflags "-X main.Version=...". @@ -16,8 +16,8 @@ var listenAddr = flag.String("listen", ":8080", "HTTP listen address") func main() { flag.Parse() carddav.Version = Version - server := sdk.NewServer(carddav.Provider()) - if err := server.ListenAndServe(*listenAddr); err != nil { + srv := server.New(carddav.Provider()) + if err := srv.ListenAndServe(*listenAddr); err != nil { log.Fatalf("server error: %v", err) } } diff --git a/go.mod b/go.mod index 38eb058..82ff426 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.happydns.org/checker-dav go 1.25.0 require ( - git.happydns.org/checker-sdk-go v1.2.0 + git.happydns.org/checker-sdk-go v1.3.0 git.happydns.org/checker-tls v0.2.0 ) diff --git a/go.sum b/go.sum index cfaede5..dd024c8 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.happydns.org/checker-sdk-go v1.2.0 h1:v4MpKAz0W3PwP+bxx3pya8w893sVH5xTD1of1cc0TV8= -git.happydns.org/checker-sdk-go v1.2.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= +git.happydns.org/checker-sdk-go v1.3.0 h1:FG2kIhlJCzI0m35EhxSgn4UWc9M4ha6aZTeoChu4l7A= +git.happydns.org/checker-sdk-go v1.3.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= git.happydns.org/checker-tls v0.2.0 h1:2dYpcePBylUc3le76fFlLbxraiLpGESmOhx4NfD7REM= git.happydns.org/checker-tls v0.2.0/go.mod h1:0ZSG0CTP007SHBPE7qInESVIOcW+xgucHUhHgj6MeZ8= github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6 h1:kHoSgklT8weIDl6R6xFpBJ5IioRdBU1v2X2aCZRVCcM= diff --git a/internal/dav/client.go b/internal/dav/client.go index 266c31c..6b03190 100644 --- a/internal/dav/client.go +++ b/internal/dav/client.go @@ -6,7 +6,7 @@ import ( ) // NewHTTPClient returns an http.Client with a sane default transport for -// probing DAV servers. TLS certificate validation uses Go's default rules — +// probing DAV servers. TLS certificate validation uses Go's default rules; // dedicated TLS correctness belongs in a separate checker. func NewHTTPClient(timeout time.Duration) *http.Client { return &http.Client{ diff --git a/internal/dav/discover.go b/internal/dav/discover.go index ce387d4..8328d23 100644 --- a/internal/dav/discover.go +++ b/internal/dav/discover.go @@ -15,7 +15,7 @@ import ( // then SRV/TXT. An explicit override shortcuts everything. // // The returned Observation.Discovery is fully populated with whatever was -// learned along the way, even if every step fails — the report leans on the +// learned along the way, even if every step fails; the report leans on the // captured evidence to tell the user which leg of the discovery broke. func Discover(ctx context.Context, client *http.Client, kind Kind, domain, explicitURL string) DiscoveryResult { res := DiscoveryResult{} @@ -26,7 +26,7 @@ func Discover(ctx context.Context, client *http.Client, kind Kind, domain, expli return res } - // 1. /.well-known — this is the #1 misconfig hotspot, so we always probe + // 1. /.well-known: this is the #1 misconfig hotspot, so we always probe // it even if SRV below might have worked, to surface the mistake. wellKnown := "https://" + domain + kind.WellKnownPath() res.WellKnownURL = wellKnown diff --git a/internal/dav/endpoints.go b/internal/dav/endpoints.go index 6063633..0297e93 100644 --- a/internal/dav/endpoints.go +++ b/internal/dav/endpoints.go @@ -15,11 +15,11 @@ import ( // A CalDAV/CardDAV context URL always implies a direct-TLS HTTPS endpoint, // so we emit a single tls.endpoint.v1 entry for the resolved context URL's // host:port. If the endpoint was reached via SRV, we also surface each SRV -// target as its own entry — those are the names operators actually need +// target as its own entry; those are the names operators actually need // certificates on, and they may differ from the queried domain. // -// SNI is always populated (equal to Host for CalDAV/CardDAV, since — unlike -// XMPP (RFC 6120 §13.7.2.1) — there is no mandated source-domain-vs-target +// SNI is always populated (equal to Host for CalDAV/CardDAV, since, unlike +// XMPP (RFC 6120 §13.7.2.1), there is no mandated source-domain-vs-target // split: clients negotiate TLS for the hostname they connect to). We fill // the field unconditionally so consumers can rely on it being set. func DiscoverEntries(obs *Observation) []sdk.DiscoveryEntry { diff --git a/internal/dav/endpoints_test.go b/internal/dav/endpoints_test.go index 35a1f5d..a40c14e 100644 --- a/internal/dav/endpoints_test.go +++ b/internal/dav/endpoints_test.go @@ -36,7 +36,7 @@ func TestDiscoverEntries_contextURLOnly(t *testing.T) { if got[0].Host != "dav.example.com" || got[0].Port != 443 { t.Errorf("unexpected endpoint: %+v", got[0]) } - // Direct TLS — no STARTTLS upgrade. + // Direct TLS; no STARTTLS upgrade. if got[0].STARTTLS != "" { t.Errorf("STARTTLS = %q, want empty (direct TLS)", got[0].STARTTLS) } diff --git a/internal/dav/options.go b/internal/dav/options.go index ebb3d91..86fd94e 100644 --- a/internal/dav/options.go +++ b/internal/dav/options.go @@ -9,7 +9,7 @@ import ( // ProbeOptions issues an HTTP OPTIONS against url and reports the parsed DAV // headers. A missing DAV: header, or one that does not contain the kind's -// required capability, is not treated as a transport error here — the caller +// required capability, is not treated as a transport error here; the caller // rule decides severity from the parsed values. func ProbeOptions(ctx context.Context, client *http.Client, url string) (OptionsResult, error) { res := OptionsResult{} diff --git a/internal/dav/options_shared.go b/internal/dav/options_shared.go index a63217e..d24035c 100644 --- a/internal/dav/options_shared.go +++ b/internal/dav/options_shared.go @@ -28,7 +28,7 @@ func UserOptions() []sdk.CheckerOptionDocumentation { Id: "context_url", Type: "string", Label: "Explicit context URL", - Description: "Optional. Bypasses /.well-known and SRV discovery — use for servers with a non-standard layout.", + Description: "Optional. Bypasses /.well-known and SRV discovery. Use for servers with a non-standard layout.", Placeholder: "https://dav.example.com/caldav/", }, } diff --git a/internal/dav/principal.go b/internal/dav/principal.go index 3d5750f..89f1f64 100644 --- a/internal/dav/principal.go +++ b/internal/dav/principal.go @@ -64,7 +64,7 @@ func FindScheduleURLs(ctx context.Context, client *http.Client, principalURL str // ── 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 +// 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"` diff --git a/internal/dav/report.go b/internal/dav/report.go index 24dc1e8..7dd6155 100644 --- a/internal/dav/report.go +++ b/internal/dav/report.go @@ -145,7 +145,7 @@ func buildCallouts(o *Observation) []calloutData { out = append(out, calloutData{ Severity: "crit", Title: fmt.Sprintf("Server does not advertise %q", o.Kind.RequiredCapability()), - Body: fmt.Sprintf("The DAV: response header is %q — this endpoint is not a %s server, or a reverse proxy is stripping headers.", strings.Join(o.Options.DAVClasses, ", "), o.Kind), + Body: fmt.Sprintf("The DAV: response header is %q. This endpoint is not a %s server, or a reverse proxy is stripping headers.", strings.Join(o.Options.DAVClasses, ", "), o.Kind), }) } if !o.HasCredentials && o.Discovery.ContextURL != "" && o.Options.HasCapability(o.Kind.RequiredCapability()) { @@ -171,7 +171,7 @@ func exampleContextURL(k Kind) string { func buildPhases(o *Observation) []phaseData { var phases []phaseData - // Phase 1 — Discovery + // Phase 1: Discovery discovery := phaseData{Title: "Discovery"} discovery.Items = append(discovery.Items, itemFor( "/.well-known redirect", @@ -205,7 +205,7 @@ func buildPhases(o *Observation) []phaseData { discovery.Open = hasItemSeverity(discovery.Items, "warn", "fail") phases = append(phases, discovery) - // Phase 2 — Transport + OPTIONS + // Phase 2: Transport + OPTIONS transport := phaseData{Title: "Transport & OPTIONS"} transport.Items = append(transport.Items, itemFor("HTTPS reached", boolStatus(o.Transport.Reached, "crit"), o.Transport.Error, ""), @@ -221,7 +221,7 @@ func buildPhases(o *Observation) []phaseData { transport.Open = hasItemSeverity(transport.Items, "warn", "fail") phases = append(phases, transport) - // Phase 3 — Authenticated + // Phase 3: Authenticated auth := phaseData{Title: "Authenticated probes"} auth.Items = append(auth.Items, authItemFor("Principal", o.Principal.URL, o.Principal.Skipped, o.Principal.Error), @@ -232,7 +232,7 @@ func buildPhases(o *Observation) []phaseData { auth.Open = hasItemSeverity(auth.Items, "warn", "fail") phases = append(phases, auth) - // Phase 4 — Scheduling (CalDAV only) + // Phase 4: Scheduling (CalDAV only) if o.Kind == KindCalDAV && o.Scheduling != nil { sched := phaseData{Title: "Scheduling (CalDAV)"} if !o.Scheduling.Advertised { @@ -259,7 +259,7 @@ func buildTLSPhase(summaries []TLSSummary) phaseData { for _, s := range summaries { label := s.Address if s.TLSVersion != "" { - label = fmt.Sprintf("%s — %s", s.Address, s.TLSVersion) + label = fmt.Sprintf("%s (%s)", s.Address, s.TLSVersion) } p.Items = append(p.Items, phaseItem{ Label: label, diff --git a/internal/dav/rules.go b/internal/dav/rules.go index dfad84d..fb72714 100644 --- a/internal/dav/rules.go +++ b/internal/dav/rules.go @@ -98,7 +98,7 @@ func (r *discoveryRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, Message: "could not resolve a context URL (no /.well-known redirect and no SRV record)", }} } - // /.well-known returning 200 is legal per RFC but strongly discouraged — + // /.well-known returning 200 is legal per RFC but strongly discouraged; // many clients won't follow it. Warn, don't crit. if disc.WellKnownCode == 200 && disc.Source != "explicit" { return []sdk.CheckState{{ @@ -250,7 +250,7 @@ func (r *collectionsRule) Evaluate(ctx context.Context, obs sdk.ObservationGette return []sdk.CheckState{{ Status: sdk.StatusWarn, Code: "collections_empty", - Message: "home-set is empty — the account has no calendars/addressbooks", + Message: "home-set is empty; the account has no calendars/addressbooks", }} } out := make([]sdk.CheckState, 0, len(c.Items)) diff --git a/internal/dav/tls_related.go b/internal/dav/tls_related.go index e07cc20..db1e8fc 100644 --- a/internal/dav/tls_related.go +++ b/internal/dav/tls_related.go @@ -17,7 +17,7 @@ import ( const TLSRelatedKey sdk.ObservationKey = "tls_probes" // tlsProbeView is a permissive decode of a TLS probe payload. We intentionally -// only read the fields we need and tolerate missing ones — the TLS checker's +// only read the fields we need and tolerate missing ones; the TLS checker's // full schema is owned by that checker. type tlsProbeView struct { Host string `json:"host,omitempty"` @@ -104,11 +104,11 @@ func (v *tlsProbeView) chainOK() (bool, bool) { // // Two payload shapes are accepted: // -// 1. {"probes": {"": , …}} — the current convention used by -// checker-tls. Each consumer picks its own probe via r.Ref — the value +// 1. {"probes": {"": , …}}: the current convention used by +// checker-tls. Each consumer picks its own probe via r.Ref; the value // is the DiscoveryEntry.Ref that the producer originally emitted, // preserved by the host along the lineage chain. -// 2. — a single top-level probe object, kept for back-compat with +// 2. : a single top-level probe object, kept for back-compat with // callers that pre-date the keyed map and with unit-test fixtures. func parseTLSRelated(r sdk.RelatedObservation) *tlsProbeView { var keyed struct { @@ -248,7 +248,7 @@ func buildTLSCallouts(v *tlsProbeView, addr string) []tlsCallout { out = append(out, tlsCallout{ Severity: "crit", Title: fmt.Sprintf("Certificate on %s has expired", addr), - Body: fmt.Sprintf("Renew it — clients will refuse to connect. Expired %d day(s) ago (valid until %s).", -days, t.Format(time.RFC3339)), + Body: fmt.Sprintf("Renew it. Clients will refuse to connect. Expired %d day(s) ago (valid until %s).", -days, t.Format(time.RFC3339)), }) case days < 14: out = append(out, tlsCallout{ diff --git a/internal/dav/tls_related_test.go b/internal/dav/tls_related_test.go index e9f9124..5f39554 100644 --- a/internal/dav/tls_related_test.go +++ b/internal/dav/tls_related_test.go @@ -65,7 +65,7 @@ func TestFoldTLSRelated_explicitIssueWinsOverFlags(t *testing.T) { {"code": "weak_cipher", "severity": "warn", "message": "TLS 1.0 offered", "fix": "disable TLS <1.2"}, }, })}) - // When explicit issues exist, we do not also emit synthesized callouts — + // When explicit issues exist, we do not also emit synthesized callouts; // the TLS checker is the source of truth for severity and wording. if len(callouts) != 1 || callouts[0].Severity != "warn" { t.Fatalf("want single warn callout, got %+v", callouts)