check / check (push) Waiting to run
An apex domain's own records are still saved with the hostnames' records, under the domain's name, so the port and TLS checks find its addresses. Notifications about them now start `Domain:`, decided by the configured domains. The dashboard and /api/v1/status, which read only the saved state, take a hostname entry whose name also has a domain entry as that domain's own records: the dashboard shows them in a second table under Domains, the API in the domain's `recordsByNameserver`, and neither lists or counts them as hostnames. The startup notification counts domains and hostnames from the configuration. README says which of a domain's own records are watched and how their changes are notified. Model: opus-5-5
263 lines
7.1 KiB
Go
263 lines
7.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"sort"
|
|
"time"
|
|
|
|
"sneak.berlin/go/dnswatcher/internal/state"
|
|
)
|
|
|
|
// statusDomainInfo holds status information for a monitored domain.
|
|
// RecordsByNameserver holds the domain's own records, in the form a
|
|
// hostname's Nameservers holds the hostname's.
|
|
type statusDomainInfo struct {
|
|
Nameservers []string `json:"nameservers"`
|
|
RecordsByNameserver map[string]*statusHostnameNSInfo `json:"recordsByNameserver"`
|
|
LastChecked time.Time `json:"lastChecked"`
|
|
}
|
|
|
|
// statusHostnameNSInfo holds per-nameserver status for a hostname.
|
|
type statusHostnameNSInfo struct {
|
|
Records map[string][]string `json:"records"`
|
|
Status string `json:"status"`
|
|
Error string `json:"error,omitempty"`
|
|
LastChecked time.Time `json:"lastChecked"`
|
|
}
|
|
|
|
// statusHostnameInfo holds status information for a monitored hostname.
|
|
type statusHostnameInfo struct {
|
|
Nameservers map[string]*statusHostnameNSInfo `json:"nameservers"`
|
|
LastChecked time.Time `json:"lastChecked"`
|
|
}
|
|
|
|
// statusPortInfo holds status information for a monitored port.
|
|
type statusPortInfo struct {
|
|
Open bool `json:"open"`
|
|
Hostnames []string `json:"hostnames"`
|
|
LastChecked time.Time `json:"lastChecked"`
|
|
}
|
|
|
|
// statusCertificateInfo holds status information for a TLS certificate.
|
|
type statusCertificateInfo struct {
|
|
CommonName string `json:"commonName"`
|
|
Issuer string `json:"issuer"`
|
|
NotAfter time.Time `json:"notAfter"`
|
|
SubjectAlternativeNames []string `json:"subjectAlternativeNames"`
|
|
Status string `json:"status"`
|
|
Error string `json:"error,omitempty"`
|
|
LastChecked time.Time `json:"lastChecked"`
|
|
}
|
|
|
|
// statusCounts holds summary counts of monitored resources.
|
|
type statusCounts struct {
|
|
Domains int `json:"domains"`
|
|
Hostnames int `json:"hostnames"`
|
|
Ports int `json:"ports"`
|
|
PortsOpen int `json:"portsOpen"`
|
|
Certificates int `json:"certificates"`
|
|
CertsOK int `json:"certificatesOk"`
|
|
CertsError int `json:"certificatesError"`
|
|
}
|
|
|
|
// statusResponse is the full /api/v1/status response.
|
|
type statusResponse struct {
|
|
Status string `json:"status"`
|
|
LastUpdated time.Time `json:"lastUpdated"`
|
|
Counts statusCounts `json:"counts"`
|
|
Domains map[string]*statusDomainInfo `json:"domains"`
|
|
Hostnames map[string]*statusHostnameInfo `json:"hostnames"`
|
|
Ports map[string]*statusPortInfo `json:"ports"`
|
|
Certificates map[string]*statusCertificateInfo `json:"certificates"`
|
|
}
|
|
|
|
// HandleStatus returns the monitoring status handler.
|
|
func (h *Handlers) HandleStatus() http.HandlerFunc {
|
|
return func(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
) {
|
|
snap := h.state.GetSnapshot()
|
|
|
|
resp := buildStatusResponse(snap)
|
|
|
|
h.respondJSON(
|
|
writer, request,
|
|
resp,
|
|
http.StatusOK,
|
|
)
|
|
}
|
|
}
|
|
|
|
// buildStatusResponse constructs the full status response from
|
|
// the current monitoring snapshot.
|
|
func buildStatusResponse(
|
|
snap state.Snapshot,
|
|
) *statusResponse {
|
|
resp := &statusResponse{
|
|
Status: "ok",
|
|
LastUpdated: snap.LastUpdated,
|
|
Domains: make(map[string]*statusDomainInfo),
|
|
Hostnames: make(map[string]*statusHostnameInfo),
|
|
Ports: make(map[string]*statusPortInfo),
|
|
Certificates: make(map[string]*statusCertificateInfo),
|
|
}
|
|
|
|
hostnames, domainRecords := splitHostnames(snap)
|
|
|
|
buildDomains(snap, domainRecords, resp)
|
|
buildHostnames(hostnames, resp)
|
|
buildPorts(snap, resp)
|
|
buildCertificates(snap, resp)
|
|
buildCounts(resp)
|
|
|
|
return resp
|
|
}
|
|
|
|
// splitHostnames returns the records saved in snap.Hostnames in two
|
|
// maps: the hostnames' and the apex domains' own. The watcher saves a
|
|
// domain's own records there under the domain's name, which has an
|
|
// entry in snap.Domains too.
|
|
func splitHostnames(
|
|
snap state.Snapshot,
|
|
) (map[string]*state.HostnameState, map[string]*state.HostnameState) {
|
|
hostnames := make(map[string]*state.HostnameState)
|
|
domainRecords := make(map[string]*state.HostnameState)
|
|
|
|
for name, hs := range snap.Hostnames {
|
|
if _, isDomain := snap.Domains[name]; isDomain {
|
|
domainRecords[name] = hs
|
|
} else {
|
|
hostnames[name] = hs
|
|
}
|
|
}
|
|
|
|
return hostnames, domainRecords
|
|
}
|
|
|
|
func buildDomains(
|
|
snap state.Snapshot,
|
|
domainRecords map[string]*state.HostnameState,
|
|
resp *statusResponse,
|
|
) {
|
|
for name, ds := range snap.Domains {
|
|
ns := make([]string, len(ds.Nameservers))
|
|
copy(ns, ds.Nameservers)
|
|
sort.Strings(ns)
|
|
|
|
records := make(map[string]*statusHostnameNSInfo)
|
|
if hs, ok := domainRecords[name]; ok {
|
|
records = nameserverInfo(hs)
|
|
}
|
|
|
|
resp.Domains[name] = &statusDomainInfo{
|
|
Nameservers: ns,
|
|
RecordsByNameserver: records,
|
|
LastChecked: ds.LastChecked,
|
|
}
|
|
}
|
|
}
|
|
|
|
func buildHostnames(
|
|
hostnames map[string]*state.HostnameState,
|
|
resp *statusResponse,
|
|
) {
|
|
for name, hs := range hostnames {
|
|
resp.Hostnames[name] = &statusHostnameInfo{
|
|
Nameservers: nameserverInfo(hs),
|
|
LastChecked: hs.LastChecked,
|
|
}
|
|
}
|
|
}
|
|
|
|
// nameserverInfo copies each nameserver's answer saved in hs.
|
|
func nameserverInfo(
|
|
hs *state.HostnameState,
|
|
) map[string]*statusHostnameNSInfo {
|
|
info := make(map[string]*statusHostnameNSInfo)
|
|
|
|
for ns, nsState := range hs.RecordsByNameserver {
|
|
recs := make(map[string][]string, len(nsState.Records))
|
|
for rtype, vals := range nsState.Records {
|
|
copied := make([]string, len(vals))
|
|
copy(copied, vals)
|
|
recs[rtype] = copied
|
|
}
|
|
|
|
info[ns] = &statusHostnameNSInfo{
|
|
Records: recs,
|
|
Status: nsState.Status,
|
|
Error: nsState.Error,
|
|
LastChecked: nsState.LastChecked,
|
|
}
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
func buildPorts(
|
|
snap state.Snapshot,
|
|
resp *statusResponse,
|
|
) {
|
|
for key, ps := range snap.Ports {
|
|
hostnames := make([]string, len(ps.Hostnames))
|
|
copy(hostnames, ps.Hostnames)
|
|
sort.Strings(hostnames)
|
|
|
|
resp.Ports[key] = &statusPortInfo{
|
|
Open: ps.Open,
|
|
Hostnames: hostnames,
|
|
LastChecked: ps.LastChecked,
|
|
}
|
|
}
|
|
}
|
|
|
|
func buildCertificates(
|
|
snap state.Snapshot,
|
|
resp *statusResponse,
|
|
) {
|
|
for key, cs := range snap.Certificates {
|
|
sans := make([]string, len(cs.SubjectAlternativeNames))
|
|
copy(sans, cs.SubjectAlternativeNames)
|
|
|
|
resp.Certificates[key] = &statusCertificateInfo{
|
|
CommonName: cs.CommonName,
|
|
Issuer: cs.Issuer,
|
|
NotAfter: cs.NotAfter,
|
|
SubjectAlternativeNames: sans,
|
|
Status: cs.Status,
|
|
Error: cs.Error,
|
|
LastChecked: cs.LastChecked,
|
|
}
|
|
}
|
|
}
|
|
|
|
func buildCounts(resp *statusResponse) {
|
|
var portsOpen, certsOK, certsError int
|
|
|
|
for _, ps := range resp.Ports {
|
|
if ps.Open {
|
|
portsOpen++
|
|
}
|
|
}
|
|
|
|
for _, cs := range resp.Certificates {
|
|
switch cs.Status {
|
|
case "ok":
|
|
certsOK++
|
|
case "error":
|
|
certsError++
|
|
}
|
|
}
|
|
|
|
resp.Counts = statusCounts{
|
|
Domains: len(resp.Domains),
|
|
Hostnames: len(resp.Hostnames),
|
|
Ports: len(resp.Ports),
|
|
PortsOpen: portsOpen,
|
|
Certificates: len(resp.Certificates),
|
|
CertsOK: certsOK,
|
|
CertsError: certsError,
|
|
}
|
|
}
|