package dav import ( "log" "net/url" "strconv" sdk "git.happydns.org/checker-sdk-go/checker" tlsct "git.happydns.org/checker-tls/contract" ) // DiscoverEntries derives TLS DiscoveryEntry records worth handing off to // downstream checkers (notably checker-tls) from a completed Observation. // // 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 // 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. func DiscoverEntries(obs *Observation) []sdk.DiscoveryEntry { if obs == nil || obs.Discovery.ContextURL == "" { return nil } var out []sdk.DiscoveryEntry 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{}{} entry, err := tlsct.NewEntry(tlsct.TLSEndpoint{ Host: host, Port: port, SNI: host, }) if err != nil { log.Printf("checker-dav: contract.NewEntry(%s:%d): %v", host, port, err) return } out = append(out, entry) } // 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 }