diff --git a/.drone-manifest.yml b/.drone-manifest.yml deleted file mode 100644 index 1046193..0000000 --- a/.drone-manifest.yml +++ /dev/null @@ -1,22 +0,0 @@ -image: happydomain/checker-matrix:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} -{{#if build.tags}} -tags: -{{#each build.tags}} - - {{this}} -{{/each}} -{{/if}} -manifests: - - image: happydomain/checker-matrix:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64 - platform: - architecture: amd64 - os: linux - - image: happydomain/checker-matrix:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64 - platform: - architecture: arm64 - os: linux - variant: v8 - - image: happydomain/checker-matrix:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm - platform: - architecture: arm - os: linux - variant: v7 diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 142d61e..0000000 --- a/.drone.yml +++ /dev/null @@ -1,187 +0,0 @@ ---- -kind: pipeline -type: docker -name: build-amd64 - -platform: - os: linux - arch: amd64 - -steps: - - name: checker build - image: golang:1-alpine - commands: - - apk add --no-cache git make - - make - environment: - CHECKER_VERSION: "${DRONE_BRANCH}-${DRONE_COMMIT}" - CGO_ENABLED: 0 - when: - event: - exclude: - - tag - - - name: checker build tag - image: golang:1-alpine - commands: - - apk add --no-cache git make - - make - environment: - CHECKER_VERSION: "${DRONE_SEMVER}" - CGO_ENABLED: 0 - when: - event: - - tag - - - name: publish on Docker Hub - image: plugins/docker - settings: - repo: happydomain/checker-matrix - auto_tag: true - auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} - dockerfile: Dockerfile - build_args: - - CHECKER_VERSION=${DRONE_BRANCH}-${DRONE_COMMIT} - username: - from_secret: docker_username - password: - from_secret: docker_password - when: - event: - exclude: - - tag - - - name: publish on Docker Hub (tag) - image: plugins/docker - settings: - repo: happydomain/checker-matrix - auto_tag: true - auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} - dockerfile: Dockerfile - build_args: - - CHECKER_VERSION=${DRONE_SEMVER} - username: - from_secret: docker_username - password: - from_secret: docker_password - when: - event: - - tag - -trigger: - branch: - exclude: - - renovate/* - event: - - cron - - push - - tag - ---- -kind: pipeline -type: docker -name: build-arm64 - -platform: - os: linux - arch: arm64 - -steps: - - name: checker build - image: golang:1-alpine - commands: - - apk add --no-cache git make - - make - environment: - CHECKER_VERSION: "${DRONE_BRANCH}-${DRONE_COMMIT}" - CGO_ENABLED: 0 - when: - event: - exclude: - - tag - - - name: checker build tag - image: golang:1-alpine - commands: - - apk add --no-cache git make - - make - environment: - CHECKER_VERSION: "${DRONE_SEMVER}" - CGO_ENABLED: 0 - when: - event: - - tag - - - name: publish on Docker Hub - image: plugins/docker - settings: - repo: happydomain/checker-matrix - auto_tag: true - auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} - dockerfile: Dockerfile - build_args: - - CHECKER_VERSION=${DRONE_BRANCH}-${DRONE_COMMIT} - username: - from_secret: docker_username - password: - from_secret: docker_password - when: - event: - exclude: - - tag - - - name: publish on Docker Hub (tag) - image: plugins/docker - settings: - repo: happydomain/checker-matrix - auto_tag: true - auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} - dockerfile: Dockerfile - build_args: - - CHECKER_VERSION=${DRONE_SEMVER} - username: - from_secret: docker_username - password: - from_secret: docker_password - when: - event: - - tag - -trigger: - event: - - cron - - push - - tag - ---- -kind: pipeline -name: docker-manifest - -platform: - os: linux - arch: arm64 - -steps: - - name: publish on Docker Hub - image: plugins/manifest - settings: - auto_tag: true - ignore_missing: true - spec: .drone-manifest.yml - username: - from_secret: docker_username - password: - from_secret: docker_password - -trigger: - branch: - exclude: - - renovate/* - event: - - cron - - push - - tag - -depends_on: - - build-amd64 - - build-arm64 diff --git a/Dockerfile b/Dockerfile index a56c3b0..447bd68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,13 +6,9 @@ WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN CGO_ENABLED=0 go build -tags standalone -ldflags "-X main.Version=${CHECKER_VERSION}" -o /checker-matrix . +RUN CGO_ENABLED=0 go build -ldflags "-X main.Version=${CHECKER_VERSION}" -o /checker-matrix . FROM scratch -COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=builder /checker-matrix /checker-matrix -USER 65534:65534 EXPOSE 8080 -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD ["/checker-matrix", "-healthcheck"] ENTRYPOINT ["/checker-matrix"] diff --git a/Makefile b/Makefile index 6a80145..faf351c 100644 --- a/Makefile +++ b/Makefile @@ -6,12 +6,12 @@ CHECKER_SOURCES := main.go $(wildcard checker/*.go) GO_LDFLAGS := -X main.Version=$(CHECKER_VERSION) -.PHONY: all plugin docker test clean +.PHONY: all plugin docker clean all: $(CHECKER_NAME) $(CHECKER_NAME): $(CHECKER_SOURCES) - go build -tags standalone -ldflags "$(GO_LDFLAGS)" -o $@ . + go build -ldflags "$(GO_LDFLAGS)" -o $@ . plugin: $(CHECKER_NAME).so @@ -21,8 +21,5 @@ $(CHECKER_NAME).so: $(CHECKER_SOURCES) $(wildcard plugin/*.go) docker: docker build --build-arg CHECKER_VERSION=$(CHECKER_VERSION) -t $(CHECKER_IMAGE) . -test: - go test -tags standalone ./... - clean: rm -f $(CHECKER_NAME) $(CHECKER_NAME).so diff --git a/README.md b/README.md index 66e5041..7be7c78 100644 --- a/README.md +++ b/README.md @@ -54,17 +54,6 @@ Set the `endpoint` admin option for the `matrixim` checker to the URL of the running checker-matrix server (e.g., `http://checker-matrix:8080`). happyDomain will delegate observation collection to this endpoint. -## Rules - -| Code | Description | Severity | -|------------------------------|---------------------------------------------------------------------------------------------------|---------------------| -| `matrix.connection_reachable`| Checks that every discovered federation endpoint accepts an inbound connection. | CRITICAL | -| `matrix.federation_ok` | Reports the overall federation status returned by the Matrix Federation Tester. | CRITICAL | -| `matrix.srv_records` | Checks that the Matrix SRV lookup (`_matrix-fed._tcp` / `_matrix._tcp`) succeeded or was skipped. | CRITICAL | -| `matrix.tls_checks` | Reviews the TLS posture on every reachable federation endpoint (chain, hostname, Ed25519 key). | CRITICAL | -| `matrix.version` | Checks that the homeserver responds to `/_matrix/federation/v1/version` with name and version. | WARNING | -| `matrix.well_known` | Checks that `/.well-known/matrix/server` (if published) is valid and points at the server_name. | CRITICAL | - ## Options | Scope | Id | Description | diff --git a/checker/collect.go b/checker/collect.go index e3175c4..9bc6340 100644 --- a/checker/collect.go +++ b/checker/collect.go @@ -4,23 +4,12 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" - "net/url" "strings" - "time" sdk "git.happydns.org/checker-sdk-go/checker" ) -const ( - defaultTesterURI = "https://federationtester.matrix.org/api/report?server_name=%s" - collectHTTPTimeout = 30 * time.Second - maxResponseBodySize = 5 << 20 // 5 MiB -) - -var collectHTTPClient = &http.Client{Timeout: collectHTTPTimeout} - func (p *matrixProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) { domain, _ := opts["serviceDomain"].(string) if domain == "" { @@ -30,20 +19,15 @@ func (p *matrixProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) ( testerURI, _ := opts["federationTesterServer"].(string) if testerURI == "" { - testerURI = defaultTesterURI - } - if !strings.Contains(testerURI, "%s") { - return nil, fmt.Errorf("federationTesterServer must contain a %%s placeholder for the domain") + testerURI = "https://federationtester.matrix.org/api/report?server_name=%s" } - reqURL := fmt.Sprintf(testerURI, url.QueryEscape(domain)) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(testerURI, domain), nil) if err != nil { return nil, fmt.Errorf("unable to build the request: %w", err) } - resp, err := collectHTTPClient.Do(req) + resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("unable to perform the test: %w", err) } @@ -54,7 +38,7 @@ func (p *matrixProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) ( } var data MatrixFederationData - if err := json.NewDecoder(io.LimitReader(resp.Body, maxResponseBodySize)).Decode(&data); err != nil { + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { return nil, fmt.Errorf("failed to decode federation tester response: %w", err) } diff --git a/checker/collect_test.go b/checker/collect_test.go deleted file mode 100644 index b5d4a4b..0000000 --- a/checker/collect_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package checker - -import ( - "context" - "net/http" - "net/http/httptest" - "strings" - "testing" - - sdk "git.happydns.org/checker-sdk-go/checker" -) - -func TestCollectMissingDomain(t *testing.T) { - p := &matrixProvider{} - if _, err := p.Collect(context.Background(), sdk.CheckerOptions{}); err == nil { - t.Fatal("expected error when serviceDomain is empty") - } -} - -func TestCollectSuccess(t *testing.T) { - const body = `{ - "WellKnownResult": {"m.server": "matrix.example.org:8448", "result": ""}, - "DNSResult": {"SRVSkipped": false, "SRVRecords": [{"Target": "matrix.example.org.", "Port": 8448, "Priority": 10, "Weight": 5}]}, - "ConnectionReports": {"1.2.3.4:8448": {"Checks": {"AllChecksOK": true, "MatchingServerName": true, "FutureValidUntilTS": true, "HasEd25519Key": true, "AllEd25519ChecksOK": true, "ValidCertificates": true}}}, - "ConnectionErrors": {}, - "Version": {"name": "Synapse", "version": "1.100.0"}, - "FederationOK": true - }` - - var gotURL string - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - gotURL = r.URL.String() - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(body)) - })) - defer srv.Close() - - p := &matrixProvider{} - out, err := p.Collect(context.Background(), sdk.CheckerOptions{ - "serviceDomain": "example.org.", - "federationTesterServer": srv.URL + "/api/report?server_name=%s", - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !strings.Contains(gotURL, "server_name=example.org") { - t.Errorf("unexpected URL %q", gotURL) - } - data, ok := out.(*MatrixFederationData) - if !ok || data == nil { - t.Fatalf("expected *MatrixFederationData, got %T", out) - } - if !data.FederationOK { - t.Error("expected FederationOK=true") - } - if data.Version.Name != "Synapse" { - t.Errorf("unexpected version name %q", data.Version.Name) - } -} - -func TestCollectNon2xx(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusBadGateway) - })) - defer srv.Close() - - p := &matrixProvider{} - _, err := p.Collect(context.Background(), sdk.CheckerOptions{ - "serviceDomain": "example.org", - "federationTesterServer": srv.URL + "/?s=%s", - }) - if err == nil { - t.Fatal("expected error on 502 response") - } -} - -func TestCollectMalformedJSON(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte("not json")) - })) - defer srv.Close() - - p := &matrixProvider{} - _, err := p.Collect(context.Background(), sdk.CheckerOptions{ - "serviceDomain": "example.org", - "federationTesterServer": srv.URL + "/?s=%s", - }) - if err == nil { - t.Fatal("expected decode error") - } -} diff --git a/checker/definition.go b/checker/definition.go index cc296fe..4e5ec7c 100644 --- a/checker/definition.go +++ b/checker/definition.go @@ -17,7 +17,7 @@ import ( var Version = "built-in" // Definition returns the CheckerDefinition for the matrix federation checker. -func (p *matrixProvider) Definition() *sdk.CheckerDefinition { +func Definition() *sdk.CheckerDefinition { return &sdk.CheckerDefinition{ ID: "matrixim", Name: "Matrix Federation Tester", @@ -50,7 +50,9 @@ func (p *matrixProvider) Definition() *sdk.CheckerDefinition { }, }, }, - Rules: Rules(), + Rules: []sdk.CheckRule{ + Rule(), + }, Interval: &sdk.CheckIntervalSpec{ Min: 5 * time.Minute, Max: 7 * 24 * time.Hour, diff --git a/checker/interactive.go b/checker/interactive.go index 9b35b79..4c00bd5 100644 --- a/checker/interactive.go +++ b/checker/interactive.go @@ -1,5 +1,3 @@ -//go:build standalone - package checker import ( @@ -10,7 +8,7 @@ import ( sdk "git.happydns.org/checker-sdk-go/checker" ) -// RenderForm implements server.Interactive. +// RenderForm implements sdk.CheckerInteractive. func (p *matrixProvider) RenderForm() []sdk.CheckerOptionField { return []sdk.CheckerOptionField{ { @@ -31,11 +29,11 @@ func (p *matrixProvider) RenderForm() []sdk.CheckerOptionField { } } -// ParseForm implements server.Interactive. +// ParseForm implements sdk.CheckerInteractive. func (p *matrixProvider) ParseForm(r *http.Request) (sdk.CheckerOptions, error) { domain := strings.TrimSpace(r.FormValue("serviceDomain")) if domain == "" { - return nil, errors.New("matrix domain is required") + return nil, errors.New("Matrix domain is required") } opts := sdk.CheckerOptions{ diff --git a/checker/provider.go b/checker/provider.go index 111b30c..de7a43d 100644 --- a/checker/provider.go +++ b/checker/provider.go @@ -14,3 +14,8 @@ type matrixProvider struct{} func (p *matrixProvider) Key() sdk.ObservationKey { return ObservationKeyMatrix } + +// Definition implements sdk.CheckerDefinitionProvider. +func (p *matrixProvider) Definition() *sdk.CheckerDefinition { + return Definition() +} diff --git a/checker/report.go b/checker/report.go index ad88a39..7952ae0 100644 --- a/checker/report.go +++ b/checker/report.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "html/template" - "sort" "strings" sdk "git.happydns.org/checker-sdk-go/checker" @@ -327,13 +326,7 @@ func (p *matrixProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error) { } // Hosts - hostNames := make([]string, 0, len(r.DNSResult.Hosts)) - for name := range r.DNSResult.Hosts { - hostNames = append(hostNames, name) - } - sort.Strings(hostNames) - for _, name := range hostNames { - h := r.DNSResult.Hosts[name] + for name, h := range r.DNSResult.Hosts { data.Hosts = append(data.Hosts, matrixHostData{ Name: name, CName: h.CName, @@ -342,13 +335,7 @@ func (p *matrixProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error) { } // Successful connections - connAddrs := make([]string, 0, len(r.ConnectionReports)) - for addr := range r.ConnectionReports { - connAddrs = append(connAddrs, addr) - } - sort.Strings(connAddrs) - for _, addr := range connAddrs { - cr := r.ConnectionReports[addr] + for addr, cr := range r.ConnectionReports { conn := matrixConnectionData{ Address: addr, TLSVersion: cr.Cipher.Version, @@ -376,15 +363,10 @@ func (p *matrixProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error) { } // Failed connections - errAddrs := make([]string, 0, len(r.ConnectionErrors)) - for addr := range r.ConnectionErrors { - errAddrs = append(errAddrs, addr) - } - sort.Strings(errAddrs) - for _, addr := range errAddrs { + for addr, ce := range r.ConnectionErrors { data.ConnectionErrors = append(data.ConnectionErrors, matrixConnErrData{ Address: addr, - Message: r.ConnectionErrors[addr].Message, + Message: ce.Message, }) } diff --git a/checker/rule.go b/checker/rule.go index 8506856..8184137 100644 --- a/checker/rule.go +++ b/checker/rule.go @@ -3,54 +3,79 @@ package checker import ( "context" "fmt" + "strings" sdk "git.happydns.org/checker-sdk-go/checker" ) -// Rules returns the full list of CheckRules exposed by the Matrix checker. -// Each rule covers a single concern so the UI can show a clear checklist -// rather than a single monolithic pass/fail line. -func Rules() []sdk.CheckRule { - return []sdk.CheckRule{ - &federationOKRule{}, - &wellKnownRule{}, - &srvRecordsRule{}, - &connectionReachableRule{}, - &tlsChecksRule{}, - &versionRule{}, - } +// Rule returns a new matrix federation check rule. +func Rule() sdk.CheckRule { + return &matrixRule{} } -// loadMatrixData fetches the Matrix observation. On error returns a -// CheckState the caller should emit to short-circuit its rule. -func loadMatrixData(ctx context.Context, obs sdk.ObservationGetter) (*MatrixFederationData, *sdk.CheckState) { +type matrixRule struct{} + +func (r *matrixRule) Name() string { + return "matrix_federation" +} + +func (r *matrixRule) Description() string { + return "Checks whether Matrix federation is working correctly" +} + +func (r *matrixRule) ValidateOptions(opts sdk.CheckerOptions) error { + return nil +} + +func (r *matrixRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState { var data MatrixFederationData if err := obs.Get(ctx, ObservationKeyMatrix, &data); err != nil { - return nil, &sdk.CheckState{ + return []sdk.CheckState{{ Status: sdk.StatusError, Message: fmt.Sprintf("Failed to get Matrix federation data: %v", err), - Code: "matrix.observation_error", - } + Code: "matrix_federation_error", + }} } - return &data, nil -} -func passState(code, message string) sdk.CheckState { - return sdk.CheckState{Status: sdk.StatusOK, Message: message, Code: code} -} + domain, _ := opts["serviceDomain"].(string) + domain = strings.TrimSuffix(domain, ".") -func infoState(code, message string) sdk.CheckState { - return sdk.CheckState{Status: sdk.StatusInfo, Message: message, Code: code} -} + if data.FederationOK { + version := strings.TrimSpace(data.Version.Name + " " + data.Version.Version) + return []sdk.CheckState{{ + Status: sdk.StatusOK, + Message: fmt.Sprintf("Running %s", version), + Code: "matrix_federation_ok", + Meta: map[string]any{ + "version": version, + }, + }} + } -func warnState(code, message string) sdk.CheckState { - return sdk.CheckState{Status: sdk.StatusWarn, Message: message, Code: code} -} + var statusLine string -func critState(code, message string) sdk.CheckState { - return sdk.CheckState{Status: sdk.StatusCrit, Message: message, Code: code} -} + if data.DNSResult.SRVError != nil && data.WellKnownResult.Result != "" { + statusLine = fmt.Sprintf("%s OR %s", data.DNSResult.SRVError.Message, data.WellKnownResult.Result) + } else if len(data.ConnectionErrors) > 0 { + var msg strings.Builder + for srv, cerr := range data.ConnectionErrors { + if msg.Len() > 0 { + msg.WriteString("; ") + } + msg.WriteString(srv) + msg.WriteString(": ") + msg.WriteString(cerr.Message) + } + statusLine = fmt.Sprintf("Connection errors: %s", msg.String()) + } else if data.WellKnownResult.Server != domain { + statusLine = fmt.Sprintf("Bad homeserver_name: got %s, expected %s", data.WellKnownResult.Server, domain) + } else { + statusLine = fmt.Sprintf("Federation broken. Check https://federationtester.matrix.org/#%s", domain) + } -func unknownState(code, message string) sdk.CheckState { - return sdk.CheckState{Status: sdk.StatusUnknown, Message: message, Code: code} + return []sdk.CheckState{{ + Status: sdk.StatusCrit, + Message: statusLine, + Code: "matrix_federation_fail", + }} } diff --git a/checker/rules_connection.go b/checker/rules_connection.go deleted file mode 100644 index 4cfa7ba..0000000 --- a/checker/rules_connection.go +++ /dev/null @@ -1,47 +0,0 @@ -package checker - -import ( - "context" - "fmt" - "sort" - - sdk "git.happydns.org/checker-sdk-go/checker" -) - -// connectionReachableRule checks that every federation endpoint returned -// by DNS accepted the TLS connection the tester attempted. -type connectionReachableRule struct{} - -func (r *connectionReachableRule) Name() string { return "matrix.connection_reachable" } -func (r *connectionReachableRule) Description() string { - return "Checks that every discovered federation endpoint accepts an inbound connection." -} - -func (r *connectionReachableRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { - data, errSt := loadMatrixData(ctx, obs) - if errSt != nil { - return []sdk.CheckState{*errSt} - } - - if len(data.ConnectionErrors) == 0 && len(data.ConnectionReports) == 0 { - return []sdk.CheckState{unknownState("matrix.connection_reachable.unknown", "No endpoint was probed by the federation tester.")} - } - - if len(data.ConnectionErrors) == 0 { - return []sdk.CheckState{passState("matrix.connection_reachable.ok", fmt.Sprintf("All %d endpoint(s) accepted the connection.", len(data.ConnectionReports)))} - } - - addrs := make([]string, 0, len(data.ConnectionErrors)) - for addr := range data.ConnectionErrors { - addrs = append(addrs, addr) - } - sort.Strings(addrs) - - out := make([]sdk.CheckState, 0, len(addrs)) - for _, addr := range addrs { - st := critState("matrix.connection_reachable.fail", data.ConnectionErrors[addr].Message) - st.Subject = addr - out = append(out, st) - } - return out -} diff --git a/checker/rules_federation.go b/checker/rules_federation.go deleted file mode 100644 index 60b8e34..0000000 --- a/checker/rules_federation.go +++ /dev/null @@ -1,67 +0,0 @@ -package checker - -import ( - "context" - "fmt" - "sort" - "strings" - - sdk "git.happydns.org/checker-sdk-go/checker" -) - -// federationOKRule reflects the overall FederationOK flag reported by the -// Matrix Federation Tester. Other rules isolate specific concerns; this -// rule is the global verdict so callers get a single-line answer to -// "does this homeserver federate?". -type federationOKRule struct{} - -func (r *federationOKRule) Name() string { return "matrix.federation_ok" } -func (r *federationOKRule) Description() string { - return "Reports the overall federation status returned by the Matrix Federation Tester." -} - -func (r *federationOKRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState { - data, errSt := loadMatrixData(ctx, obs) - if errSt != nil { - return []sdk.CheckState{*errSt} - } - - domain, _ := opts["serviceDomain"].(string) - domain = strings.TrimSuffix(domain, ".") - - if data.FederationOK { - version := strings.TrimSpace(data.Version.Name + " " + data.Version.Version) - st := passState("matrix.federation_ok.ok", "Matrix federation is working.") - if version != "" { - st.Message = fmt.Sprintf("Matrix federation is working (running %s).", version) - st.Meta = map[string]any{"version": version} - } - return []sdk.CheckState{st} - } - - var statusLine string - switch { - case data.DNSResult.SRVError != nil && data.WellKnownResult.Result != "": - statusLine = fmt.Sprintf("%s; %s", data.DNSResult.SRVError.Message, data.WellKnownResult.Result) - case len(data.ConnectionErrors) > 0: - srvs := make([]string, 0, len(data.ConnectionErrors)) - for srv := range data.ConnectionErrors { - srvs = append(srvs, srv) - } - sort.Strings(srvs) - var msg strings.Builder - for _, srv := range srvs { - if msg.Len() > 0 { - msg.WriteString("; ") - } - msg.WriteString(srv) - msg.WriteString(": ") - msg.WriteString(data.ConnectionErrors[srv].Message) - } - statusLine = fmt.Sprintf("Connection errors: %s", msg.String()) - default: - statusLine = fmt.Sprintf("Federation broken. Check https://federationtester.matrix.org/#%s", domain) - } - - return []sdk.CheckState{critState("matrix.federation_ok.fail", statusLine)} -} diff --git a/checker/rules_srv.go b/checker/rules_srv.go deleted file mode 100644 index e42de47..0000000 --- a/checker/rules_srv.go +++ /dev/null @@ -1,48 +0,0 @@ -package checker - -import ( - "context" - "fmt" - - sdk "git.happydns.org/checker-sdk-go/checker" -) - -// srvRecordsRule checks _matrix-fed._tcp / _matrix._tcp SRV delegation: was -// the lookup successful, and does it yield at least one record (or was it -// legitimately skipped because of a CNAME/well-known path)? -type srvRecordsRule struct{} - -func (r *srvRecordsRule) Name() string { return "matrix.srv_records" } -func (r *srvRecordsRule) Description() string { - return "Checks that the Matrix SRV lookup (_matrix-fed._tcp / _matrix._tcp) succeeded or was legitimately skipped." -} - -func (r *srvRecordsRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { - data, errSt := loadMatrixData(ctx, obs) - if errSt != nil { - return []sdk.CheckState{*errSt} - } - - dns := data.DNSResult - - if dns.SRVError != nil { - return []sdk.CheckState{critState("matrix.srv_records.error", fmt.Sprintf("SRV lookup error: %s", dns.SRVError.Message))} - } - - if dns.SRVSkipped { - msg := "SRV lookup skipped by the federation tester." - if dns.SRVCName != "" { - msg = fmt.Sprintf("SRV lookup skipped (CNAME: %s).", dns.SRVCName) - } - return []sdk.CheckState{unknownState("matrix.srv_records.skipped", msg)} - } - - if len(dns.SRVRecords) == 0 { - return []sdk.CheckState{infoState( - "matrix.srv_records.absent", - "No Matrix SRV records published (federation may still work via well-known).", - )} - } - - return []sdk.CheckState{passState("matrix.srv_records.ok", fmt.Sprintf("%d SRV record(s) published.", len(dns.SRVRecords)))} -} diff --git a/checker/rules_test.go b/checker/rules_test.go deleted file mode 100644 index c865d58..0000000 --- a/checker/rules_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package checker - -import ( - "context" - "encoding/json" - "strings" - "testing" - - sdk "git.happydns.org/checker-sdk-go/checker" -) - -type stubObs struct { - data *MatrixFederationData - err error -} - -func (s stubObs) Get(_ context.Context, _ sdk.ObservationKey, dest any) error { - if s.err != nil { - return s.err - } - b, err := json.Marshal(s.data) - if err != nil { - return err - } - return json.Unmarshal(b, dest) -} - -func (s stubObs) GetRelated(_ context.Context, _ sdk.ObservationKey) ([]sdk.RelatedObservation, error) { - return nil, nil -} - -func eval(t *testing.T, rule sdk.CheckRule, data *MatrixFederationData, opts sdk.CheckerOptions) []sdk.CheckState { - t.Helper() - return rule.Evaluate(context.Background(), stubObs{data: data}, opts) -} - -func TestFederationOKRulePass(t *testing.T) { - data := &MatrixFederationData{FederationOK: true} - data.Version.Name = "Synapse" - data.Version.Version = "1.100.0" - got := eval(t, &federationOKRule{}, data, sdk.CheckerOptions{"serviceDomain": "example.org."}) - if len(got) != 1 || got[0].Status != sdk.StatusOK { - t.Fatalf("expected single OK state, got %+v", got) - } - if !strings.Contains(got[0].Message, "Synapse 1.100.0") { - t.Errorf("expected version in message, got %q", got[0].Message) - } -} - -func TestFederationOKRuleFailDeterministicOrder(t *testing.T) { - data := &MatrixFederationData{} - data.ConnectionErrors = map[string]struct { - Message string `json:"Message"` - }{ - "z.example:8448": {Message: "boom z"}, - "a.example:8448": {Message: "boom a"}, - "m.example:8448": {Message: "boom m"}, - } - first := eval(t, &federationOKRule{}, data, nil)[0].Message - for range 5 { - if eval(t, &federationOKRule{}, data, nil)[0].Message != first { - t.Fatal("federation_ok message not stable across runs") - } - } - idxA := strings.Index(first, "a.example") - idxM := strings.Index(first, "m.example") - idxZ := strings.Index(first, "z.example") - if !(idxA < idxM && idxM < idxZ) { - t.Errorf("expected sorted order a 0 { - msg = fmt.Sprintf("TLS checks failed: %s.", strings.Join(problems, "; ")) - } - st := critState("matrix.tls_checks.fail", msg) - st.Subject = addr - out = append(out, st) - } - return out -} diff --git a/checker/rules_version.go b/checker/rules_version.go deleted file mode 100644 index 552dd68..0000000 --- a/checker/rules_version.go +++ /dev/null @@ -1,40 +0,0 @@ -package checker - -import ( - "context" - "fmt" - "strings" - - sdk "git.happydns.org/checker-sdk-go/checker" -) - -// versionRule reports whether the federation tester could fetch the -// homeserver version string. The test probe reaches /_matrix/federation/v1/version, -// so a failure here hints at a federation-path problem even when the rest -// of the federation handshake looks healthy. -type versionRule struct{} - -func (r *versionRule) Name() string { return "matrix.version" } -func (r *versionRule) Description() string { - return "Checks that the homeserver responds to /_matrix/federation/v1/version and reports its name and version." -} - -func (r *versionRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { - data, errSt := loadMatrixData(ctx, obs) - if errSt != nil { - return []sdk.CheckState{*errSt} - } - - if data.Version.Error != "" { - return []sdk.CheckState{warnState("matrix.version.error", fmt.Sprintf("Homeserver /version probe failed: %s", data.Version.Error))} - } - - version := strings.TrimSpace(data.Version.Name + " " + data.Version.Version) - if version == "" { - return []sdk.CheckState{infoState("matrix.version.unknown", "Homeserver did not return a version string.")} - } - - st := passState("matrix.version.ok", fmt.Sprintf("Homeserver running %s.", version)) - st.Meta = map[string]any{"version": version} - return []sdk.CheckState{st} -} diff --git a/checker/rules_wellknown.go b/checker/rules_wellknown.go deleted file mode 100644 index 163693d..0000000 --- a/checker/rules_wellknown.go +++ /dev/null @@ -1,43 +0,0 @@ -package checker - -import ( - "context" - "fmt" - "strings" - - sdk "git.happydns.org/checker-sdk-go/checker" -) - -// wellKnownRule checks the /.well-known/matrix/server delegation: was a -// delegation published, did it resolve, and does it point back at the -// expected server_name? -type wellKnownRule struct{} - -func (r *wellKnownRule) Name() string { return "matrix.well_known" } -func (r *wellKnownRule) Description() string { - return "Checks that /.well-known/matrix/server (if published) is valid and points at the expected server_name." -} - -func (r *wellKnownRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState { - data, errSt := loadMatrixData(ctx, obs) - if errSt != nil { - return []sdk.CheckState{*errSt} - } - - wk := data.WellKnownResult - - // Nothing published: the host may rely on SRV only. Mark informational. - if wk.Server == "" && wk.Result == "" { - return []sdk.CheckState{infoState("matrix.well_known.absent", "No /.well-known/matrix/server delegation published (federation may still work via SRV).")} - } - - // Published but the tester flagged an error string. - if wk.Server == "" && wk.Result != "" { - if strings.Contains(strings.ToLower(wk.Result), "no .well-known") { - return []sdk.CheckState{unknownState("matrix.well_known.absent", "No /.well-known/matrix/server delegation found (federation may still work via SRV).")} - } - return []sdk.CheckState{critState("matrix.well_known.error", fmt.Sprintf("Well-known delegation error: %s", wk.Result))} - } - - return []sdk.CheckState{passState("matrix.well_known.ok", fmt.Sprintf("Well-known delegation resolves to %s.", wk.Server))} -} diff --git a/go.mod b/go.mod index 53001e2..8c5c78c 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module git.happydns.org/checker-matrix go 1.25.0 -require git.happydns.org/checker-sdk-go v1.5.0 +require git.happydns.org/checker-sdk-go v1.2.0 diff --git a/go.sum b/go.sum index c389c68..272600a 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -git.happydns.org/checker-sdk-go v1.5.0 h1:5uD5Cm6xJ+lwnhbJ09iCXGHbYS9zRh+Yh0NeBHkAPBY= -git.happydns.org/checker-sdk-go v1.5.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= +git.happydns.org/checker-sdk-go v1.2.0 h1:v4MpKAz0W3PwP+bxx3pya8w893sVH5xTD1of1cc0TV8= +git.happydns.org/checker-sdk-go v1.2.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= diff --git a/main.go b/main.go index 22aae3a..2d19c41 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,7 @@ import ( "log" matrix "git.happydns.org/checker-matrix/checker" - "git.happydns.org/checker-sdk-go/checker/server" + sdk "git.happydns.org/checker-sdk-go/checker" ) // Version is the standalone binary's version. It defaults to "custom-build" @@ -23,8 +23,8 @@ func main() { // CheckerDefinition.Version. matrix.Version = Version - srv := server.New(matrix.Provider()) - if err := srv.ListenAndServe(*listenAddr); err != nil { + server := sdk.NewServer(matrix.Provider()) + if err := server.ListenAndServe(*listenAddr); err != nil { log.Fatalf("server error: %v", err) } } diff --git a/plugin/plugin.go b/plugin/plugin.go index c0dc6dd..3ce631f 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -20,6 +20,5 @@ var Version = "custom-build" // that the host will register in its global registries. func NewCheckerPlugin() (*sdk.CheckerDefinition, sdk.ObservationProvider, error) { matrix.Version = Version - prvd := matrix.Provider() - return prvd.(sdk.CheckerDefinitionProvider).Definition(), prvd, nil + return matrix.Definition(), matrix.Provider(), nil }