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.
149 lines
4.9 KiB
Go
149 lines
4.9 KiB
Go
// Package dav holds code shared by the CalDAV and CardDAV checkers:
|
|
// discovery, OPTIONS probing, PROPFIND helpers, and report rendering.
|
|
package dav
|
|
|
|
import "time"
|
|
|
|
// Kind is carried end-to-end through a run so shared helpers branch on it
|
|
// rather than duplicating per-protocol code.
|
|
type Kind string
|
|
|
|
const (
|
|
KindCalDAV Kind = "caldav"
|
|
KindCardDAV Kind = "carddav"
|
|
)
|
|
|
|
// ServiceName returns the RFC 6764 SRV label, with the leading "_" but
|
|
// without the "_tcp" suffix.
|
|
func (k Kind) ServiceName(secure bool) string {
|
|
switch k {
|
|
case KindCalDAV:
|
|
if secure {
|
|
return "_caldavs"
|
|
}
|
|
return "_caldav"
|
|
case KindCardDAV:
|
|
if secure {
|
|
return "_carddavs"
|
|
}
|
|
return "_carddav"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (k Kind) WellKnownPath() string {
|
|
return "/.well-known/" + string(k)
|
|
}
|
|
|
|
// RequiredCapability is the DAV: header token a compliant server must
|
|
// advertise.
|
|
func (k Kind) RequiredCapability() string {
|
|
switch k {
|
|
case KindCalDAV:
|
|
return "calendar-access"
|
|
case KindCardDAV:
|
|
return "addressbook"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Observation is what each checker persists. Scheduling is CalDAV-only and
|
|
// left nil for CardDAV.
|
|
type Observation struct {
|
|
Kind Kind `json:"kind"`
|
|
Domain string `json:"domain"`
|
|
HasCredentials bool `json:"has_credentials"`
|
|
Discovery DiscoveryResult `json:"discovery"`
|
|
Transport TransportResult `json:"transport"`
|
|
Options OptionsResult `json:"options"`
|
|
Principal PrincipalResult `json:"principal"`
|
|
HomeSet HomeSetResult `json:"home_set"`
|
|
Collections CollectionsResult `json:"collections"`
|
|
Report ReportResult `json:"report"`
|
|
Scheduling *SchedulingResult `json:"scheduling,omitempty"`
|
|
CollectedAt time.Time `json:"collected_at"`
|
|
}
|
|
|
|
type SRVRecord struct {
|
|
Target string `json:"target"`
|
|
Port uint16 `json:"port"`
|
|
Priority uint16 `json:"priority"`
|
|
Weight uint16 `json:"weight"`
|
|
}
|
|
|
|
// DiscoveryResult records every signal seen during lookup, even on failure,
|
|
// so the report can pinpoint which leg of discovery broke.
|
|
type DiscoveryResult struct {
|
|
SecureSRV []SRVRecord `json:"secure_srv,omitempty"`
|
|
PlaintextSRV []SRVRecord `json:"plaintext_srv,omitempty"`
|
|
SRVError string `json:"srv_error,omitempty"`
|
|
TXTPath string `json:"txt_path,omitempty"`
|
|
TXTError string `json:"txt_error,omitempty"`
|
|
WellKnownURL string `json:"well_known_url,omitempty"`
|
|
WellKnownCode int `json:"well_known_code,omitempty"`
|
|
WellKnownChain []string `json:"well_known_chain,omitempty"`
|
|
WellKnownError string `json:"well_known_error,omitempty"`
|
|
ContextURL string `json:"context_url,omitempty"`
|
|
Source string `json:"source,omitempty"` // "explicit", "well-known", "srv-txt"
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// TransportResult is intentionally minimal: cert validation is out of scope
|
|
// here, a dedicated TLS checker owns it.
|
|
type TransportResult struct {
|
|
Reached bool `json:"reached"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
type OptionsResult struct {
|
|
StatusCode int `json:"status_code"`
|
|
DAVClasses []string `json:"dav_classes,omitempty"`
|
|
AllowMethods []string `json:"allow_methods,omitempty"`
|
|
AuthSchemes []string `json:"auth_schemes,omitempty"`
|
|
Server string `json:"server,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// PrincipalResult.Skipped is set when no credentials were supplied; the
|
|
// rule turns that into StatusUnknown rather than a failure.
|
|
type PrincipalResult struct {
|
|
Skipped bool `json:"skipped,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
type HomeSetResult struct {
|
|
Skipped bool `json:"skipped,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
type CollectionInfo struct {
|
|
Path string `json:"path"`
|
|
Name string `json:"name,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
MaxResourceSize int64 `json:"max_resource_size,omitempty"`
|
|
SupportedComponentSet []string `json:"supported_component_set,omitempty"` // CalDAV only
|
|
SupportedAddressData []string `json:"supported_address_data,omitempty"` // CardDAV only
|
|
}
|
|
|
|
type CollectionsResult struct {
|
|
Skipped bool `json:"skipped,omitempty"`
|
|
Items []CollectionInfo `json:"items,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
type ReportResult struct {
|
|
Skipped bool `json:"skipped,omitempty"`
|
|
QueryOK bool `json:"query_ok,omitempty"`
|
|
ProbePath string `json:"probe_path,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// SchedulingResult is CalDAV-only.
|
|
type SchedulingResult struct {
|
|
Advertised bool `json:"advertised"`
|
|
InboxURL string `json:"inbox_url,omitempty"`
|
|
OutboxURL string `json:"outbox_url,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|