package checker import ( "context" "fmt" "slices" "strings" sdk "git.happydns.org/checker-sdk-go/checker" ) // validateXMPPOptions is the shared options validator for both the provider // and the aggregate rule. func validateXMPPOptions(opts sdk.CheckerOptions) error { if v, ok := opts["mode"]; ok { if s, ok := v.(string); ok && s != "" && !slices.Contains(validModes, s) { return fmt.Errorf(`mode must be "c2s", "s2s", or "both"`) } } return nil } // ValidateOptions implements sdk.OptionsValidator on the provider. func (p *xmppProvider) ValidateOptions(opts sdk.CheckerOptions) error { return validateXMPPOptions(opts) } // xmppRule is a minimal back-compat aggregate rule. Newer deployments should // prefer the split per-concern rules exposed by Rules(); this one is kept so // existing tests that compose a single-status output keep working. type xmppRule struct{} func (r *xmppRule) Name() string { return "xmpp_server" } func (r *xmppRule) Description() string { return "Aggregate XMPP posture (prefer the per-concern rules)." } func (r *xmppRule) ValidateOptions(opts sdk.CheckerOptions) error { return validateXMPPOptions(opts) } func (r *xmppRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState { data, errSt := loadXMPPData(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } wantC2S, wantS2S := wantsFromOpts(opts) issues := deriveIssues(data, wantC2S, wantS2S) // Fold related TLS observations into the aggregate so cert/chain // problems surface on the XMPP service page. related, _ := obs.GetRelated(ctx, TLSRelatedKey) issues = append(issues, tlsIssuesFromRelated(related)...) worst := sdk.StatusOK var critMsgs, warnMsgs []string var firstCritCode, firstWarnCode string for _, is := range issues { switch is.Severity { case SeverityCrit: if worst < sdk.StatusCrit { worst = sdk.StatusCrit } if firstCritCode == "" { firstCritCode = is.Code } critMsgs = append(critMsgs, is.Message) case SeverityWarn: if worst < sdk.StatusWarn { worst = sdk.StatusWarn } if firstWarnCode == "" { firstWarnCode = is.Code } warnMsgs = append(warnMsgs, is.Message) } } if (wantC2S && !data.Coverage.WorkingC2S) || (wantS2S && !data.Coverage.WorkingS2S) { if worst < sdk.StatusCrit { worst = sdk.StatusCrit } var missing []string if wantC2S && !data.Coverage.WorkingC2S { missing = append(missing, "c2s") } if wantS2S && !data.Coverage.WorkingS2S { missing = append(missing, "s2s") } critMsgs = append(critMsgs, "no working "+joinModes(missing)+" endpoint") if firstCritCode == "" { firstCritCode = CodeAllEndpointsDown } } meta := map[string]any{ "working_c2s": data.Coverage.WorkingC2S, "working_s2s": data.Coverage.WorkingS2S, "has_ipv4": data.Coverage.HasIPv4, "has_ipv6": data.Coverage.HasIPv6, "endpoints": len(data.Endpoints), "issue_count": len(issues), } switch worst { case sdk.StatusOK: return []sdk.CheckState{{ Status: sdk.StatusOK, Message: fmt.Sprintf("XMPP operational (c2s=%v, s2s=%v, %d endpoints)", data.Coverage.WorkingC2S, data.Coverage.WorkingS2S, len(data.Endpoints)), Code: "xmpp.ok", Meta: meta, }} case sdk.StatusWarn: return []sdk.CheckState{{ Status: sdk.StatusWarn, Message: "XMPP works with warnings: " + joinTop(warnMsgs, 2), Code: firstWarnCode, Meta: meta, }} default: return []sdk.CheckState{{ Status: sdk.StatusCrit, Message: "XMPP broken: " + joinTop(critMsgs, 2), Code: firstCritCode, Meta: meta, }} } } func joinModes(ms []string) string { switch len(ms) { case 0: return "" case 1: return ms[0] default: return ms[0] + "/" + ms[1] } } func joinTop(msgs []string, n int) string { if len(msgs) == 0 { return "" } if len(msgs) <= n { return strings.Join(msgs, "; ") } return strings.Join(msgs[:n], "; ") + fmt.Sprintf(" (+%d more)", len(msgs)-n) }