Follow the checker-sdk-go interface consolidation: reporter methods now take sdk.ReportContext and read the payload via ctx.Data() instead of the raw json.RawMessage parameter. Backed by the same underlying logic.
378 lines
9.9 KiB
Go
378 lines
9.9 KiB
Go
package checker
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"strings"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
// ── 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(ctx sdk.ReportContext) (string, error) {
|
|
var r MatrixFederationData
|
|
if err := json.Unmarshal(ctx.Data(), &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
|
|
}
|