package checker import ( "encoding/json" "fmt" "html/template" "sort" "strings" "time" sdk "git.happydns.org/checker-sdk-go/checker" ) type reportFix struct { Severity string Code string Message string Fix string Endpoint string } type reportEndpoint struct { Mode string ModeLabel string SRVPrefix string Target string Port uint16 Address string IsIPv6 bool DirectTLS bool TCPConnected bool StreamOpened bool STARTTLSOffered bool STARTTLSRequired bool STARTTLSUpgraded bool TLSVersion string TLSCipher string SASLMechanisms []string DialbackOffered bool SASLExternal bool StreamFrom string ElapsedMS int64 Error string // TLS posture (from a related tls_probes observation, when available). TLSPosture *reportTLSPosture // Rendering helpers. AnyFail bool StatusLabel string StatusClass string } type reportTLSPosture struct { CheckedAt time.Time ChainValid *bool HostnameMatch *bool NotAfter time.Time Issues []reportFix } type reportSRVEntry struct { Prefix string Target string Port uint16 Priority uint16 Weight uint16 IPv4 []string IPv6 []string } type reportData struct { Domain string RunAt string StatusLabel string StatusClass string HasIssues bool Fixes []reportFix SRV []reportSRVEntry FallbackProbed bool JabberLegacy bool Endpoints []reportEndpoint HasIPv4 bool HasIPv6 bool WorkingC2S bool WorkingS2S bool HasTLSPosture bool } var reportTpl = template.Must(template.New("xmpp").Funcs(template.FuncMap{ "hasPrefix": strings.HasPrefix, "deref": func(b *bool) bool { return b != nil && *b }, }).Parse(` XMPP Report: {{.Domain}}

XMPP: {{.Domain}}

{{.StatusLabel}}
{{if .WorkingC2S}}c2s OK{{else}}c2s FAIL{{end}} {{if .WorkingS2S}}s2s OK{{else}}s2s FAIL{{end}} {{if .HasIPv4}}IPv4{{end}} {{if .HasIPv6}}IPv6{{end}}
Checked {{.RunAt}}
{{if .HasIssues}}

What to fix

{{range .Fixes}}
{{.Code}}{{if .Endpoint}} · {{.Endpoint}}{{end}}
{{.Message}}
{{if .Fix}}
→ {{.Fix}}
{{end}}
{{end}}
{{end}}

DNS / SRV

{{if .FallbackProbed}}

No SRV records published; fell back to probing the bare domain on default ports.

{{else if .SRV}} {{range .SRV}} {{end}}
RecordTargetPortPrio/WeightIPv4IPv6
{{.Prefix}} {{.Target}} {{.Port}} {{.Priority}}/{{.Weight}} {{range .IPv4}}{{.}} {{end}} {{range .IPv6}}{{.}} {{end}}
{{else}}

No SRV records found.

{{end}} {{if .JabberLegacy}}

⚠ Obsolete _jabber._tcp records are still published.

{{end}}
{{if .Endpoints}}

Endpoints ({{len .Endpoints}})

{{range .Endpoints}} {{.ModeLabel}} · {{.Address}}{{if .DirectTLS}} · direct-TLS{{end}} {{.StatusLabel}}
SRV
{{.SRVPrefix}}{{.Target}}:{{.Port}}
Family
{{if .IsIPv6}}IPv6{{else}}IPv4{{end}}
TCP
{{if .TCPConnected}}✓ connected{{else}}✗ failed{{end}}
Stream
{{if .StreamOpened}}✓ opened{{if .StreamFrom}} (from={{.StreamFrom}}){{end}}{{else}}✗ not opened{{end}}
{{if not .DirectTLS}}
STARTTLS
{{if .STARTTLSOffered}}✓ offered{{else}}✗ not offered{{end}} {{if .STARTTLSRequired}} · required{{else if .STARTTLSOffered}} · not required{{end}}
{{end}}
TLS
{{if .STARTTLSUpgraded}}✓ {{.TLSVersion}}{{if .TLSCipher}} — {{.TLSCipher}}{{end}}{{else}}✗ no TLS{{end}}
{{if eq .Mode "c2s"}}
SASL
{{if .SASLMechanisms}}
{{range .SASLMechanisms}}{{.}}{{end}}
{{else}}none advertised{{end}}
{{end}} {{if eq .Mode "s2s"}}
Federation
{{if .DialbackOffered}}✓ dialback{{else}}✗ no dialback{{end}} · {{if .SASLExternal}}✓ SASL EXTERNAL{{else}}✗ no SASL EXTERNAL{{end}}
{{end}} {{with .TLSPosture}}
TLS cert
{{if .ChainValid}} {{if deref .ChainValid}}✓ chain valid{{else}}✗ chain invalid{{end}} {{end}} {{if .HostnameMatch}} · {{if deref .HostnameMatch}}✓ hostname match{{else}}✗ hostname mismatch{{end}} {{end}} {{if not .NotAfter.IsZero}} · expires {{.NotAfter.Format "2006-01-02"}} {{end}} {{if not .CheckedAt.IsZero}}
TLS checked {{.CheckedAt.Format "2006-01-02 15:04 MST"}}
{{end}} {{range .Issues}}
{{.Code}}
{{.Message}}
{{if .Fix}}
→ {{.Fix}}
{{end}}
{{end}}
{{end}}
Duration
{{.ElapsedMS}} ms
{{if .Error}}
Error
{{.Error}}
{{end}}
{{end}}
{{end}} `)) // GetHTMLReport implements sdk.CheckerHTMLReporter. It folds in related TLS // observations so the XMPP service page shows cert posture directly, without // the user having to open a separate TLS report. func (p *xmppProvider) GetHTMLReport(rctx sdk.ReportContext) (string, error) { var d XMPPData if err := json.Unmarshal(rctx.Data(), &d); err != nil { return "", fmt.Errorf("unmarshal xmpp observation: %w", err) } view := buildReportData(&d, rctx.Related(TLSRelatedKey)) return renderReport(view) } func renderReport(view reportData) (string, error) { var buf strings.Builder if err := reportTpl.Execute(&buf, view); err != nil { return "", fmt.Errorf("render xmpp report: %w", err) } return buf.String(), nil } func buildReportData(d *XMPPData, related []sdk.RelatedObservation) reportData { tlsIssues := tlsIssuesFromRelated(related) tlsByAddr := indexTLSByAddress(related) allIssues := append([]Issue(nil), d.Issues...) allIssues = append(allIssues, tlsIssues...) view := reportData{ Domain: d.Domain, RunAt: d.RunAt, FallbackProbed: d.SRV.FallbackProbed, JabberLegacy: len(d.SRV.Jabber) > 0, HasIPv4: d.Coverage.HasIPv4, HasIPv6: d.Coverage.HasIPv6, WorkingC2S: d.Coverage.WorkingC2S, WorkingS2S: d.Coverage.WorkingS2S, HasIssues: len(allIssues) > 0, HasTLSPosture: len(tlsByAddr) > 0, } // Status banner. worst := SeverityInfo for _, is := range allIssues { if is.Severity == SeverityCrit { worst = SeverityCrit break } if is.Severity == SeverityWarn { worst = SeverityWarn } } if len(allIssues) == 0 { view.StatusLabel = "OK" view.StatusClass = "ok" } else { switch worst { case SeverityCrit: view.StatusLabel = "FAIL" view.StatusClass = "fail" case SeverityWarn: view.StatusLabel = "WARN" view.StatusClass = "warn" default: view.StatusLabel = "INFO" view.StatusClass = "muted" } } // Fix list: sort crit → warn → info, preserving order within each severity. sevRank := func(s string) int { switch s { case SeverityCrit: return 0 case SeverityWarn: return 1 default: return 2 } } sort.SliceStable(allIssues, func(i, j int) bool { return sevRank(allIssues[i].Severity) < sevRank(allIssues[j].Severity) }) for _, is := range allIssues { view.Fixes = append(view.Fixes, reportFix{ Severity: is.Severity, Code: is.Code, Message: is.Message, Fix: is.Fix, Endpoint: is.Endpoint, }) } // SRV rows. addSRV := func(prefix string, records []SRVRecord) { for _, r := range records { view.SRV = append(view.SRV, reportSRVEntry{ Prefix: prefix, Target: r.Target, Port: r.Port, Priority: r.Priority, Weight: r.Weight, IPv4: r.IPv4, IPv6: r.IPv6, }) } } addSRV("_xmpp-client._tcp", d.SRV.Client) addSRV("_xmpp-server._tcp", d.SRV.Server) addSRV("_xmpps-client._tcp", d.SRV.ClientSecure) addSRV("_xmpps-server._tcp", d.SRV.ServerSecure) addSRV("_jabber._tcp", d.SRV.Jabber) // Endpoint rows. for _, ep := range d.Endpoints { re := reportEndpoint{ Mode: string(ep.Mode), ModeLabel: modeLabel(ep.Mode), SRVPrefix: ep.SRVPrefix, Target: ep.Target, Port: ep.Port, Address: ep.Address, IsIPv6: ep.IsIPv6, DirectTLS: ep.DirectTLS, TCPConnected: ep.TCPConnected, StreamOpened: ep.StreamOpened, STARTTLSOffered: ep.STARTTLSOffered, STARTTLSRequired: ep.STARTTLSRequired, STARTTLSUpgraded: ep.STARTTLSUpgraded, TLSVersion: ep.TLSVersion, TLSCipher: ep.TLSCipher, SASLMechanisms: ep.SASLMechanisms, DialbackOffered: ep.DialbackOffered, SASLExternal: ep.SASLExternal, StreamFrom: ep.StreamFrom, ElapsedMS: ep.ElapsedMS, Error: ep.Error, } if meta, hit := tlsByAddr[ep.Address]; hit { re.TLSPosture = meta } else if meta, hit := tlsByAddr[endpointKey(ep.Target, ep.Port)]; hit { re.TLSPosture = meta } ok := ep.TCPConnected && ep.STARTTLSUpgraded if ep.Mode == ModeServer { ok = ok && (ep.DialbackOffered || ep.SASLExternal) } if ep.Mode == ModeClient { ok = ok && len(ep.SASLMechanisms) > 0 } re.AnyFail = !ok if ok { re.StatusLabel = "OK" re.StatusClass = "ok" } else if ep.TCPConnected { re.StatusLabel = "partial" re.StatusClass = "warn" } else { re.StatusLabel = "unreachable" re.StatusClass = "fail" } view.Endpoints = append(view.Endpoints, re) } return view } func modeLabel(m XMPPMode) string { switch m { case ModeClient: return "client" case ModeServer: return "server" default: return string(m) } } // indexTLSByAddress returns a map keyed by "host:port" (and by the SRV // target:port when host is the target) pointing at a reportTLSPosture. // This lets the template match a related observation to the right endpoint. func indexTLSByAddress(related []sdk.RelatedObservation) map[string]*reportTLSPosture { out := map[string]*reportTLSPosture{} for _, r := range related { v := parseTLSRelated(r) if v == nil { continue } addr := v.address() if addr == "" { continue } posture := &reportTLSPosture{ CheckedAt: r.CollectedAt, ChainValid: v.ChainValid, HostnameMatch: v.HostnameMatch, NotAfter: v.NotAfter, } for _, is := range v.Issues { sev := strings.ToLower(is.Severity) if sev != SeverityCrit && sev != SeverityWarn && sev != SeverityInfo { continue } posture.Issues = append(posture.Issues, reportFix{ Severity: sev, Code: is.Code, Message: is.Message, Fix: is.Fix, }) } out[addr] = posture } return out }