checker-xmpp/checker/rule.go

147 lines
3.8 KiB
Go

package checker
import (
"context"
"fmt"
"slices"
"strings"
sdk "git.happydns.org/checker-sdk-go/checker"
)
func Rule() sdk.CheckRule {
return &xmppRule{}
}
type xmppRule struct{}
func (r *xmppRule) Name() string {
return "xmpp_server"
}
func (r *xmppRule) Description() string {
return "Checks discovery, STARTTLS, SASL and federation auth of an XMPP server"
}
func (r *xmppRule) ValidateOptions(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
}
func (r *xmppRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) sdk.CheckState {
var data XMPPData
if err := obs.Get(ctx, ObservationKeyXMPP, &data); err != nil {
return sdk.CheckState{
Status: sdk.StatusError,
Message: fmt.Sprintf("failed to load XMPP observation: %v", err),
Code: "xmpp.observation_error",
}
}
issues := append([]Issue(nil), data.Issues...)
// Fold related TLS observations (from a downstream TLS checker, if any)
// into the XMPP issue list so cert/chain problems show up on the XMPP
// service page without requiring a separate glance at the TLS checker.
related, _ := obs.GetRelated(ctx, TLSRelatedKey)
issues = append(issues, tlsIssuesFromRelated(related)...)
// Reduce issue list to the worst severity.
worst := sdk.StatusOK
critMsgs, warnMsgs := []string{}, []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)
}
}
mode, _ := sdk.GetOption[string](opts, "mode")
if mode == "" {
mode = "both"
}
wantC2S := mode != "s2s"
wantS2S := mode != "c2s"
// Even without issues, the check isn't OK unless we got at least one
// working endpoint in each requested mode.
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 "+strings.Join(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(data.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 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)
}