checker-dav/internal/dav/endpoints.go
Pierre-Olivier Mercier df4a7a89d1 checker: adopt unified ReportContext reporter signature
Follow the checker-sdk-go interface consolidation: reporter methods
now take sdk.ReportContext and read the payload via ctx.Data() instead
of the raw json.RawMessage parameter. Backed by the same underlying
logic — this is a signature migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:41:17 +07:00

92 lines
2.5 KiB
Go

package dav
import (
"net/url"
"strconv"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// DiscoverEndpoints derives TLS endpoints worth handing off to downstream
// checkers (notably the dedicated TLS checker) from a completed Observation.
//
// A CalDAV/CardDAV context URL always implies a direct-TLS HTTPS endpoint, so
// we emit a single `tls` 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 endpoint — 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
// split: clients negotiate TLS for the hostname they connect to). We fill
// the field unconditionally so consumers can rely on it being set, matching
// the convention already used by the XMPP checker.
func DiscoverEndpoints(obs *Observation) []sdk.DiscoveredEndpoint {
if obs == nil || obs.Discovery.ContextURL == "" {
return nil
}
var out []sdk.DiscoveredEndpoint
seen := map[string]struct{}{}
add := func(host string, port uint16) {
if host == "" || port == 0 {
return
}
key := host + ":" + strconv.Itoa(int(port))
if _, dup := seen[key]; dup {
return
}
seen[key] = struct{}{}
out = append(out, sdk.DiscoveredEndpoint{
Type: "tls",
Host: host,
Port: port,
SNI: host,
})
}
// Primary endpoint: the resolved context URL.
if host, port, ok := hostPortFromURL(obs.Discovery.ContextURL); ok {
add(host, port)
}
// Secondary endpoints: every TLS SRV target. Clients may connect to any
// of them per weight/priority, and all of them need a valid certificate.
for _, r := range obs.Discovery.SecureSRV {
port := r.Port
if port == 0 {
port = 443
}
add(r.Target, port)
}
return out
}
// hostPortFromURL extracts the (host, port) pair from an absolute URL. The
// port defaults to 443 for https and 80 for http. Returns ok=false for
// malformed URLs so callers can silently skip them.
func hostPortFromURL(raw string) (host string, port uint16, ok bool) {
u, err := url.Parse(raw)
if err != nil {
return "", 0, false
}
host = u.Hostname()
if host == "" {
return "", 0, false
}
if p := u.Port(); p != "" {
n, convErr := strconv.ParseUint(p, 10, 16)
if convErr != nil {
return "", 0, false
}
return host, uint16(n), true
}
switch u.Scheme {
case "https":
return host, 443, true
case "http":
return host, 80, true
}
return "", 0, false
}