Initial commit
This commit is contained in:
commit
2bb91d33d4
16 changed files with 878 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
checker-matrix
|
||||
*.so
|
||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
ARG CHECKER_VERSION=custom-build
|
||||
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-X main.Version=${CHECKER_VERSION}" -o /checker-matrix .
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /checker-matrix /checker-matrix
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/checker-matrix"]
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 The happyDomain Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the “Software”), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
25
Makefile
Normal file
25
Makefile
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
CHECKER_NAME := checker-matrix
|
||||
CHECKER_IMAGE := happydomain/$(CHECKER_NAME)
|
||||
CHECKER_VERSION ?= custom-build
|
||||
|
||||
CHECKER_SOURCES := main.go $(wildcard checker/*.go)
|
||||
|
||||
GO_LDFLAGS := -X main.Version=$(CHECKER_VERSION)
|
||||
|
||||
.PHONY: all plugin docker clean
|
||||
|
||||
all: $(CHECKER_NAME)
|
||||
|
||||
$(CHECKER_NAME): $(CHECKER_SOURCES)
|
||||
go build -ldflags "$(GO_LDFLAGS)" -o $@ .
|
||||
|
||||
plugin: $(CHECKER_NAME).so
|
||||
|
||||
$(CHECKER_NAME).so: $(CHECKER_SOURCES) $(wildcard plugin/*.go)
|
||||
go build -buildmode=plugin -ldflags "$(GO_LDFLAGS)" -o $@ ./plugin/
|
||||
|
||||
docker:
|
||||
docker build --build-arg CHECKER_VERSION=$(CHECKER_VERSION) -t $(CHECKER_IMAGE) .
|
||||
|
||||
clean:
|
||||
rm -f $(CHECKER_NAME) $(CHECKER_NAME).so
|
||||
26
NOTICE
Normal file
26
NOTICE
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
checker-matrix
|
||||
Copyright (c) 2026 The happyDomain Authors
|
||||
|
||||
This product is licensed under the MIT License (see LICENSE).
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
Third-party notices
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
This product includes software developed as part of the checker-sdk-go
|
||||
project (https://git.happydns.org/happyDomain/checker-sdk-go), licensed
|
||||
under the Apache License, Version 2.0:
|
||||
|
||||
checker-sdk-go
|
||||
Copyright 2020-2026 The happyDomain Authors
|
||||
|
||||
This product includes software developed as part of the happyDomain
|
||||
project (https://happydomain.org).
|
||||
|
||||
Portions of this code were originally written for the happyDomain
|
||||
server (licensed under AGPL-3.0 and a commercial license) and are
|
||||
made available there under the Apache License, Version 2.0 to enable
|
||||
a permissively licensed ecosystem of checker plugins.
|
||||
|
||||
You may obtain a copy of the Apache License 2.0 at:
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
71
README.md
Normal file
71
README.md
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# checker-matrix
|
||||
|
||||
Matrix federation checker for [happyDomain](https://www.happydomain.org/).
|
||||
|
||||
Queries a [Matrix Federation Tester](https://federationtester.matrix.org/)
|
||||
instance to verify that a Matrix homeserver is correctly federating, stores
|
||||
the full report as an observation, and renders a rich HTML summary
|
||||
(connections, certificates, well-known, DNS/SRV resolution).
|
||||
|
||||
## Usage
|
||||
|
||||
### Standalone HTTP server
|
||||
|
||||
```bash
|
||||
make
|
||||
./checker-matrix -listen :8080
|
||||
```
|
||||
|
||||
The server exposes the standard happyDomain external checker endpoints
|
||||
(`/health`, `/definition`, `/collect`, `/evaluate`, `/html-report`).
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
make docker
|
||||
docker run -p 8080:8080 happydomain/checker-matrix
|
||||
```
|
||||
|
||||
### happyDomain plugin
|
||||
|
||||
```bash
|
||||
make plugin
|
||||
# produces checker-matrix.so, loadable by happyDomain as a Go plugin
|
||||
```
|
||||
|
||||
The plugin exposes a `NewCheckerPlugin` symbol returning the checker
|
||||
definition and observation provider, which happyDomain registers in its
|
||||
global registries at load time.
|
||||
|
||||
### Versioning
|
||||
|
||||
The binary, plugin, and Docker image embed a version string overridable
|
||||
at build time:
|
||||
|
||||
```bash
|
||||
make CHECKER_VERSION=1.2.3
|
||||
make plugin CHECKER_VERSION=1.2.3
|
||||
make docker CHECKER_VERSION=1.2.3
|
||||
```
|
||||
|
||||
### happyDomain remote endpoint
|
||||
|
||||
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.
|
||||
|
||||
## Options
|
||||
|
||||
| Scope | Id | Description |
|
||||
| ----- | ------------------------ | -------------------------------------------------------------------------- |
|
||||
| Run | `serviceDomain` | Matrix domain to test (auto-filled, default `matrix.org`) |
|
||||
| Admin | `federationTesterServer` | Federation Tester URL template (default: `https://federationtester.matrix.org/api/report?server_name=%s`) |
|
||||
|
||||
The checker only applies to services of type `abstract.MatrixIM`.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the **MIT License** (see `LICENSE`). The
|
||||
third-party Apache-2.0 attributions for `checker-sdk-go` are recorded in
|
||||
`NOTICE` and must accompany any binary or source redistribution of this
|
||||
project.
|
||||
46
checker/collect.go
Normal file
46
checker/collect.go
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
func (p *matrixProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) {
|
||||
domain, _ := opts["serviceDomain"].(string)
|
||||
if domain == "" {
|
||||
return nil, fmt.Errorf("serviceDomain is required")
|
||||
}
|
||||
domain = strings.TrimSuffix(domain, ".")
|
||||
|
||||
testerURI, _ := opts["federationTesterServer"].(string)
|
||||
if testerURI == "" {
|
||||
testerURI = "https://federationtester.matrix.org/api/report?server_name=%s"
|
||||
}
|
||||
|
||||
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 := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to perform the test: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("federation tester returned status %d; check https://federationtester.matrix.org/#%s", resp.StatusCode, domain)
|
||||
}
|
||||
|
||||
var data MatrixFederationData
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode federation tester response: %w", err)
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
62
checker/definition.go
Normal file
62
checker/definition.go
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// Version is the checker version reported in CheckerDefinition.Version.
|
||||
//
|
||||
// It defaults to "built-in", which is appropriate when the checker package is
|
||||
// imported directly (built-in or plugin mode). Standalone binaries (like
|
||||
// main.go) should override this from their own Version variable at the start
|
||||
// of main(), which makes it easy for CI to inject a version with a single
|
||||
// -ldflags "-X main.Version=..." flag instead of targeting the nested
|
||||
// package path.
|
||||
var Version = "built-in"
|
||||
|
||||
// Definition returns the CheckerDefinition for the matrix federation checker.
|
||||
func Definition() *sdk.CheckerDefinition {
|
||||
return &sdk.CheckerDefinition{
|
||||
ID: "matrixim",
|
||||
Name: "Matrix Federation Tester",
|
||||
Version: Version,
|
||||
Availability: sdk.CheckerAvailability{
|
||||
ApplyToService: true,
|
||||
LimitToServices: []string{"abstract.MatrixIM"},
|
||||
},
|
||||
HasHTMLReport: true,
|
||||
ObservationKeys: []sdk.ObservationKey{ObservationKeyMatrix},
|
||||
Options: sdk.CheckerOptionsDocumentation{
|
||||
RunOpts: []sdk.CheckerOptionDocumentation{
|
||||
{
|
||||
Id: "serviceDomain",
|
||||
Type: "string",
|
||||
Label: "Matrix domain",
|
||||
Placeholder: "matrix.org",
|
||||
Default: "matrix.org",
|
||||
AutoFill: sdk.AutoFillDomainName,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
AdminOpts: []sdk.CheckerOptionDocumentation{
|
||||
{
|
||||
Id: "federationTesterServer",
|
||||
Type: "string",
|
||||
Label: "Federation Tester Server",
|
||||
Placeholder: "https://federationtester.matrix.org/api/report?server_name=%s",
|
||||
Default: "https://federationtester.matrix.org/api/report?server_name=%s",
|
||||
},
|
||||
},
|
||||
},
|
||||
Rules: []sdk.CheckRule{
|
||||
Rule(),
|
||||
},
|
||||
Interval: &sdk.CheckIntervalSpec{
|
||||
Min: 5 * time.Minute,
|
||||
Max: 7 * 24 * time.Hour,
|
||||
Default: 24 * time.Hour,
|
||||
},
|
||||
}
|
||||
}
|
||||
21
checker/provider.go
Normal file
21
checker/provider.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// Provider returns a new matrix federation observation provider.
|
||||
func Provider() sdk.ObservationProvider {
|
||||
return &matrixProvider{}
|
||||
}
|
||||
|
||||
type matrixProvider struct{}
|
||||
|
||||
func (p *matrixProvider) Key() sdk.ObservationKey {
|
||||
return ObservationKeyMatrix
|
||||
}
|
||||
|
||||
// Definition implements sdk.CheckerDefinitionProvider.
|
||||
func (p *matrixProvider) Definition() *sdk.CheckerDefinition {
|
||||
return Definition()
|
||||
}
|
||||
376
checker/report.go
Normal file
376
checker/report.go
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ── HTML report ───────────────────────────────────────────────────────────────
|
||||
|
||||
type matrixCertData struct {
|
||||
SubjectCommonName string
|
||||
IssuerCommonName string
|
||||
SHA256Fingerprint string
|
||||
DNSNames []string
|
||||
}
|
||||
|
||||
type matrixConnectionData struct {
|
||||
Address string
|
||||
TLSVersion string
|
||||
CipherSuite string
|
||||
Certs []matrixCertData
|
||||
AllChecksOK bool
|
||||
CheckDetails []matrixCheckItem
|
||||
Errors []string
|
||||
Open bool
|
||||
}
|
||||
|
||||
type matrixCheckItem struct {
|
||||
Label string
|
||||
OK bool
|
||||
}
|
||||
|
||||
type matrixConnErrData struct {
|
||||
Address string
|
||||
Message string
|
||||
}
|
||||
|
||||
type matrixSRVRecord struct {
|
||||
Target string
|
||||
Port uint16
|
||||
Priority uint16
|
||||
Weight uint16
|
||||
}
|
||||
|
||||
type matrixHostData struct {
|
||||
Name string
|
||||
CName string
|
||||
Addrs []string
|
||||
}
|
||||
|
||||
type matrixTemplateData struct {
|
||||
FederationOK bool
|
||||
Version string
|
||||
VersionError string
|
||||
WellKnownServer string
|
||||
WellKnownResult string
|
||||
SRVSkipped bool
|
||||
SRVCName string
|
||||
SRVRecords []matrixSRVRecord
|
||||
SRVError string
|
||||
Hosts []matrixHostData
|
||||
Addrs []string
|
||||
Connections []matrixConnectionData
|
||||
ConnectionErrors []matrixConnErrData
|
||||
}
|
||||
|
||||
var matrixHTMLTemplate = template.Must(
|
||||
template.New("matrix").Parse(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Matrix Federation Report</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
:root {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #1f2937;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
body { margin: 0; padding: 1rem; }
|
||||
code { font-family: ui-monospace, monospace; font-size: .9em; }
|
||||
h2 { font-size: 1rem; font-weight: 700; margin: 0 0 .6rem; }
|
||||
h3 { font-size: .9rem; font-weight: 600; margin: 0 0 .4rem; }
|
||||
|
||||
.hd {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: .75rem;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.08);
|
||||
}
|
||||
.hd h1 { margin: 0 0 .4rem; font-size: 1.15rem; font-weight: 700; }
|
||||
|
||||
.badge {
|
||||
display: inline-flex; align-items: center;
|
||||
padding: .2em .65em;
|
||||
border-radius: 9999px;
|
||||
font-size: .78rem; font-weight: 700;
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
.ok { background: #d1fae5; color: #065f46; }
|
||||
.fail { background: #fee2e2; color: #991b1b; }
|
||||
|
||||
.version { color: #6b7280; font-size: .82rem; margin-top: .35rem; }
|
||||
|
||||
.section {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: .85rem 1rem;
|
||||
margin-bottom: .6rem;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.07);
|
||||
}
|
||||
|
||||
details {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: .45rem;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.07);
|
||||
overflow: hidden;
|
||||
}
|
||||
.section details {
|
||||
box-shadow: none;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e5e7eb;
|
||||
margin-bottom: .4rem;
|
||||
}
|
||||
summary {
|
||||
display: flex; align-items: center; gap: .5rem;
|
||||
padding: .65rem 1rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
list-style: none;
|
||||
}
|
||||
summary::-webkit-details-marker { display: none; }
|
||||
summary::before {
|
||||
content: "▶";
|
||||
font-size: .65rem;
|
||||
color: #9ca3af;
|
||||
transition: transform .15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
details[open] > summary::before { transform: rotate(90deg); }
|
||||
.conn-addr { font-weight: 600; flex: 1; font-size: .9rem; font-family: ui-monospace, monospace; }
|
||||
|
||||
.details-body { padding: .6rem 1rem .85rem; border-top: 1px solid #f3f4f6; }
|
||||
|
||||
table { border-collapse: collapse; width: 100%; font-size: .85rem; }
|
||||
th, td { text-align: left; padding: .3rem .5rem; border-bottom: 1px solid #f3f4f6; }
|
||||
th { font-weight: 600; color: #6b7280; }
|
||||
|
||||
.check-ok { color: #059669; }
|
||||
.check-fail { color: #dc2626; }
|
||||
|
||||
.errmsg { color: #dc2626; font-size: .85rem; margin: .25rem 0 0; }
|
||||
.note { color: #6b7280; font-size: .85rem; }
|
||||
|
||||
ul { margin: .25rem 0; padding-left: 1.2rem; }
|
||||
li { margin-bottom: .15rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="hd">
|
||||
<h1>Matrix Federation</h1>
|
||||
{{if .FederationOK}}
|
||||
<span class="badge ok">Federation OK</span>
|
||||
{{- else}}
|
||||
<span class="badge fail">Federation FAIL</span>
|
||||
{{- end}}
|
||||
{{if .Version}}<div class="version">Server: <code>{{.Version}}</code>{{if .VersionError}} — {{.VersionError}}{{end}}</div>{{end}}
|
||||
</div>
|
||||
|
||||
{{if .Connections}}
|
||||
<div class="section">
|
||||
<h2>Connections ({{len .Connections}})</h2>
|
||||
{{range .Connections}}
|
||||
<details{{if .Open}} open{{end}}>
|
||||
<summary>
|
||||
<span class="conn-addr">{{.Address}}</span>
|
||||
{{if .AllChecksOK}}<span class="badge ok">All checks OK</span>{{else}}<span class="badge fail">Checks failed</span>{{end}}
|
||||
</summary>
|
||||
<div class="details-body">
|
||||
{{if or .TLSVersion .CipherSuite}}
|
||||
<h3>TLS</h3>
|
||||
<p class="note">{{.TLSVersion}}{{if and .TLSVersion .CipherSuite}} — {{end}}{{.CipherSuite}}</p>
|
||||
{{end}}
|
||||
|
||||
{{if .Certs}}
|
||||
<h3>Certificates</h3>
|
||||
<table>
|
||||
<tr><th>Subject</th><th>Issuer</th><th>DNS Names</th><th>Fingerprint (SHA-256)</th></tr>
|
||||
{{range .Certs}}
|
||||
<tr>
|
||||
<td><code>{{.SubjectCommonName}}</code></td>
|
||||
<td><code>{{.IssuerCommonName}}</code></td>
|
||||
<td>{{range .DNSNames}}<code>{{.}}</code> {{end}}</td>
|
||||
<td><code>{{.SHA256Fingerprint}}</code></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{end}}
|
||||
|
||||
{{if .CheckDetails}}
|
||||
<h3 style="margin-top:.7rem">Checks</h3>
|
||||
<table>
|
||||
{{range .CheckDetails}}
|
||||
<tr>
|
||||
<td>{{if .OK}}<span class="check-ok">✓</span>{{else}}<span class="check-fail">✗</span>{{end}}</td>
|
||||
<td>{{.Label}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{end}}
|
||||
|
||||
{{range .Errors}}<p class="errmsg">⚠ {{.}}</p>{{end}}
|
||||
</div>
|
||||
</details>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .ConnectionErrors}}
|
||||
<div class="section">
|
||||
<h2>Connection Errors ({{len .ConnectionErrors}})</h2>
|
||||
{{range .ConnectionErrors}}
|
||||
<p><code>{{.Address}}</code><br><span class="errmsg">{{.Message}}</span></p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="section">
|
||||
<h2>Well-Known</h2>
|
||||
{{if .WellKnownServer}}
|
||||
<p>Server: <code>{{.WellKnownServer}}</code></p>
|
||||
{{else if .WellKnownResult}}
|
||||
<p class="note">{{.WellKnownResult}}</p>
|
||||
{{else}}
|
||||
<p class="note">Not found.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>DNS Resolution</h2>
|
||||
{{if .SRVSkipped}}
|
||||
<p class="note">SRV lookup skipped{{if .SRVCName}} (CNAME: <code>{{.SRVCName}}</code>){{end}}</p>
|
||||
{{else if .SRVError}}
|
||||
<p class="errmsg">SRV error: {{.SRVError}}</p>
|
||||
{{else if .SRVRecords}}
|
||||
<h3>SRV Records</h3>
|
||||
<table>
|
||||
<tr><th>Target</th><th>Port</th><th>Priority</th><th>Weight</th></tr>
|
||||
{{range .SRVRecords}}
|
||||
<tr>
|
||||
<td><code>{{.Target}}</code></td>
|
||||
<td>{{.Port}}</td>
|
||||
<td>{{.Priority}}</td>
|
||||
<td>{{.Weight}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{else}}
|
||||
<p class="note">No SRV records found.</p>
|
||||
{{end}}
|
||||
|
||||
{{if .Hosts}}
|
||||
<h3 style="margin-top:.6rem">Resolved Hosts</h3>
|
||||
{{range .Hosts}}
|
||||
<p style="margin:.25rem 0">
|
||||
<code>{{.Name}}</code>
|
||||
{{if .CName}} → <code>{{.CName}}</code>{{end}}
|
||||
{{if .Addrs}}: {{range .Addrs}}<code>{{.}}</code> {{end}}{{end}}
|
||||
</p>
|
||||
{{end}}
|
||||
{{else if .Addrs}}
|
||||
<h3 style="margin-top:.6rem">Addresses</h3>
|
||||
<ul>{{range .Addrs}}<li><code>{{.}}</code></li>{{end}}</ul>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>`),
|
||||
)
|
||||
|
||||
// GetHTMLReport implements sdk.CheckerHTMLReporter.
|
||||
func (p *matrixProvider) GetHTMLReport(raw json.RawMessage) (string, error) {
|
||||
var r MatrixFederationData
|
||||
if err := json.Unmarshal(raw, &r); err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal matrix report: %w", err)
|
||||
}
|
||||
|
||||
data := matrixTemplateData{
|
||||
FederationOK: r.FederationOK,
|
||||
WellKnownServer: r.WellKnownResult.Server,
|
||||
WellKnownResult: r.WellKnownResult.Result,
|
||||
SRVSkipped: r.DNSResult.SRVSkipped,
|
||||
SRVCName: r.DNSResult.SRVCName,
|
||||
Addrs: r.DNSResult.Addrs,
|
||||
}
|
||||
|
||||
// Version
|
||||
if r.Version.Name != "" || r.Version.Version != "" {
|
||||
data.Version = strings.TrimSpace(r.Version.Name + " " + r.Version.Version)
|
||||
}
|
||||
data.VersionError = r.Version.Error
|
||||
|
||||
// SRV records
|
||||
for _, s := range r.DNSResult.SRVRecords {
|
||||
data.SRVRecords = append(data.SRVRecords, matrixSRVRecord{
|
||||
Target: s.Target,
|
||||
Port: s.Port,
|
||||
Priority: s.Priority,
|
||||
Weight: s.Weight,
|
||||
})
|
||||
}
|
||||
|
||||
// SRV error
|
||||
if r.DNSResult.SRVError != nil {
|
||||
data.SRVError = r.DNSResult.SRVError.Message
|
||||
}
|
||||
|
||||
// Hosts
|
||||
for name, h := range r.DNSResult.Hosts {
|
||||
data.Hosts = append(data.Hosts, matrixHostData{
|
||||
Name: name,
|
||||
CName: h.CName,
|
||||
Addrs: h.Addrs,
|
||||
})
|
||||
}
|
||||
|
||||
// Successful connections
|
||||
for addr, cr := range r.ConnectionReports {
|
||||
conn := matrixConnectionData{
|
||||
Address: addr,
|
||||
TLSVersion: cr.Cipher.Version,
|
||||
CipherSuite: cr.Cipher.CipherSuite,
|
||||
AllChecksOK: cr.Checks.AllChecksOK,
|
||||
Errors: cr.Errors,
|
||||
Open: !cr.Checks.AllChecksOK,
|
||||
}
|
||||
for _, cert := range cr.Certificates {
|
||||
conn.Certs = append(conn.Certs, matrixCertData{
|
||||
SubjectCommonName: cert.SubjectCommonName,
|
||||
IssuerCommonName: cert.IssuerCommonName,
|
||||
SHA256Fingerprint: cert.SHA256Fingerprint,
|
||||
DNSNames: cert.DNSNames,
|
||||
})
|
||||
}
|
||||
conn.CheckDetails = []matrixCheckItem{
|
||||
{"Matching server name", cr.Checks.MatchingServerName},
|
||||
{"Certificate valid until future", cr.Checks.FutureValidUntilTS},
|
||||
{"Valid certificates", cr.Checks.ValidCertificates},
|
||||
{"Has Ed25519 key", cr.Checks.HasEd25519Key},
|
||||
{"All Ed25519 checks OK", cr.Checks.AllEd25519ChecksOK},
|
||||
}
|
||||
data.Connections = append(data.Connections, conn)
|
||||
}
|
||||
|
||||
// Failed connections
|
||||
for addr, ce := range r.ConnectionErrors {
|
||||
data.ConnectionErrors = append(data.ConnectionErrors, matrixConnErrData{
|
||||
Address: addr,
|
||||
Message: ce.Message,
|
||||
})
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
if err := matrixHTMLTemplate.Execute(&buf, data); err != nil {
|
||||
return "", fmt.Errorf("failed to render matrix HTML report: %w", err)
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
81
checker/rule.go
Normal file
81
checker/rule.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// Rule returns a new matrix federation check rule.
|
||||
func Rule() sdk.CheckRule {
|
||||
return &matrixRule{}
|
||||
}
|
||||
|
||||
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 sdk.CheckState{
|
||||
Status: sdk.StatusError,
|
||||
Message: fmt.Sprintf("Failed to get Matrix federation data: %v", err),
|
||||
Code: "matrix_federation_error",
|
||||
}
|
||||
}
|
||||
|
||||
domain, _ := opts["serviceDomain"].(string)
|
||||
domain = strings.TrimSuffix(domain, ".")
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var statusLine string
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return sdk.CheckState{
|
||||
Status: sdk.StatusCrit,
|
||||
Message: statusLine,
|
||||
Code: "matrix_federation_fail",
|
||||
}
|
||||
}
|
||||
72
checker/types.go
Normal file
72
checker/types.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// Package checker implements the Matrix federation checker for happyDomain.
|
||||
//
|
||||
// It queries a Matrix Federation Tester instance (by default the public one
|
||||
// hosted at https://federationtester.matrix.org) to verify that a Matrix
|
||||
// homeserver is correctly federating, then exposes the result both as an
|
||||
// observation and as a rich HTML report.
|
||||
package checker
|
||||
|
||||
import (
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// ObservationKeyMatrix is the observation key for Matrix federation test data.
|
||||
const ObservationKeyMatrix sdk.ObservationKey = "matrix_federation"
|
||||
|
||||
// MatrixFederationData is the full payload returned by the Matrix Federation
|
||||
// Tester API and stored as the observation.
|
||||
type MatrixFederationData struct {
|
||||
WellKnownResult struct {
|
||||
Server string `json:"m.server"`
|
||||
Result string `json:"result"`
|
||||
CacheExpiresAt int64 `json:"CacheExpiresAt"`
|
||||
} `json:"WellKnownResult"`
|
||||
DNSResult struct {
|
||||
SRVSkipped bool `json:"SRVSkipped"`
|
||||
SRVCName string `json:"SRVCName"`
|
||||
SRVRecords []struct {
|
||||
Target string `json:"Target"`
|
||||
Port uint16 `json:"Port"`
|
||||
Priority uint16 `json:"Priority"`
|
||||
Weight uint16 `json:"Weight"`
|
||||
} `json:"SRVRecords"`
|
||||
SRVError *struct {
|
||||
Message string `json:"Message"`
|
||||
} `json:"SRVError"`
|
||||
Hosts map[string]struct {
|
||||
CName string `json:"CName"`
|
||||
Addrs []string `json:"Addrs"`
|
||||
} `json:"Hosts"`
|
||||
Addrs []string `json:"Addrs"`
|
||||
} `json:"DNSResult"`
|
||||
ConnectionReports map[string]struct {
|
||||
Certificates []struct {
|
||||
SubjectCommonName string `json:"SubjectCommonName"`
|
||||
IssuerCommonName string `json:"IssuerCommonName"`
|
||||
SHA256Fingerprint string `json:"SHA256Fingerprint"`
|
||||
DNSNames []string `json:"DNSNames"`
|
||||
} `json:"Certificates"`
|
||||
Cipher struct {
|
||||
Version string `json:"Version"`
|
||||
CipherSuite string `json:"CipherSuite"`
|
||||
} `json:"Cipher"`
|
||||
Checks struct {
|
||||
AllChecksOK bool `json:"AllChecksOK"`
|
||||
MatchingServerName bool `json:"MatchingServerName"`
|
||||
FutureValidUntilTS bool `json:"FutureValidUntilTS"`
|
||||
HasEd25519Key bool `json:"HasEd25519Key"`
|
||||
AllEd25519ChecksOK bool `json:"AllEd25519ChecksOK"`
|
||||
ValidCertificates bool `json:"ValidCertificates"`
|
||||
} `json:"Checks"`
|
||||
Errors []string `json:"Errors"`
|
||||
} `json:"ConnectionReports"`
|
||||
ConnectionErrors map[string]struct {
|
||||
Message string `json:"Message"`
|
||||
} `json:"ConnectionErrors"`
|
||||
Version struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Error string `json:"error,omitempty"`
|
||||
} `json:"Version"`
|
||||
FederationOK bool `json:"FederationOK"`
|
||||
}
|
||||
5
go.mod
Normal file
5
go.mod
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module git.happydns.org/checker-matrix
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require git.happydns.org/checker-sdk-go v0.0.1
|
||||
2
go.sum
Normal file
2
go.sum
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
git.happydns.org/checker-sdk-go v0.0.1 h1:4RxCJr73HWKxjOyU/6NJMO8lXJmH0gMLA68EzTqLbQI=
|
||||
git.happydns.org/checker-sdk-go v0.0.1/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI=
|
||||
30
main.go
Normal file
30
main.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
|
||||
matrix "git.happydns.org/checker-matrix/checker"
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// Version is the standalone binary's version. It defaults to "custom-build"
|
||||
// and is meant to be overridden by the CI at link time:
|
||||
//
|
||||
// go build -ldflags "-X main.Version=1.2.3" .
|
||||
var Version = "custom-build"
|
||||
|
||||
var listenAddr = flag.String("listen", ":8080", "HTTP listen address")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
// Propagate the binary version to the checker package so it shows up in
|
||||
// CheckerDefinition.Version.
|
||||
matrix.Version = Version
|
||||
|
||||
server := sdk.NewServer(matrix.Provider())
|
||||
if err := server.ListenAndServe(*listenAddr); err != nil {
|
||||
log.Fatalf("server error: %v", err)
|
||||
}
|
||||
}
|
||||
24
plugin/plugin.go
Normal file
24
plugin/plugin.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Command plugin is the happyDomain plugin entrypoint for the matrix checker.
|
||||
//
|
||||
// It is built as a Go plugin (`go build -buildmode=plugin`) and loaded at
|
||||
// runtime by happyDomain.
|
||||
package main
|
||||
|
||||
import (
|
||||
matrix "git.happydns.org/checker-matrix/checker"
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// Version is the plugin's version. It defaults to "custom-build" and is
|
||||
// meant to be overridden by the CI at link time:
|
||||
//
|
||||
// go build -buildmode=plugin -ldflags "-X main.Version=1.2.3" -o checker-matrix.so ./plugin
|
||||
var Version = "custom-build"
|
||||
|
||||
// NewCheckerPlugin is the symbol resolved by happyDomain when loading the
|
||||
// .so file. It returns the checker definition and the observation provider
|
||||
// that the host will register in its global registries.
|
||||
func NewCheckerPlugin() (*sdk.CheckerDefinition, sdk.ObservationProvider, error) {
|
||||
matrix.Version = Version
|
||||
return matrix.Definition(), matrix.Provider(), nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue