check / check (push) Successful in 1m20s
When a domain's parent zone's servers answer NXDOMAIN, LookupNS returns ErrNXDomain. The watcher then saves the domain with no nameservers and nxdomain set, shown on the dashboard and in /api/v1/status, asks for none of its records and removes those saved, so its old nameservers go in one NS Change. A domain with no delegation of its own gets an empty set and its records are still asked at the zone it is in. FindAuthoritativeNameservers moves to a parent name only on one of those two answers; when the servers do not answer, it returns the error. After an upgrade, a domain without its own delegation that was saved with its parent zone's nameservers gets one NS Change; the README says so. Model: opus-5-5
283 lines
7.8 KiB
Go
283 lines
7.8 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. NXDomain is true when
|
|
// the domain's parent zone's servers answered that it does not exist.
|
|
type statusDomainInfo struct {
|
|
Nameservers []string `json:"nameservers"`
|
|
RecordsByNameserver map[string]*statusHostnameNSInfo `json:"recordsByNameserver"`
|
|
NXDomain bool `json:"nxdomain"`
|
|
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.
|
|
// Domains and Hostnames list the apex domains and the hostnames that
|
|
// resolve to its address.
|
|
type statusPortInfo struct {
|
|
Open bool `json:"open"`
|
|
Domains []string `json:"domains"`
|
|
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),
|
|
Certificates: make(map[string]*statusCertificateInfo),
|
|
}
|
|
|
|
hostnames, domainRecords := splitHostnames(snap)
|
|
|
|
buildDomains(snap, domainRecords, resp)
|
|
buildHostnames(hostnames, resp)
|
|
resp.Ports = buildPorts(snap)
|
|
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,
|
|
NXDomain: ds.NXDomain,
|
|
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
|
|
}
|
|
|
|
// buildPorts returns the port entries saved in snap. A port entry
|
|
// saves apex domains with its hostnames; they are told apart as in
|
|
// splitHostnames, by a domain entry in snap.Domains.
|
|
func buildPorts(snap state.Snapshot) map[string]*statusPortInfo {
|
|
ports := make(map[string]*statusPortInfo, len(snap.Ports))
|
|
|
|
for key, ps := range snap.Ports {
|
|
domains := []string{}
|
|
hostnames := []string{}
|
|
|
|
for _, name := range ps.Hostnames {
|
|
if _, isDomain := snap.Domains[name]; isDomain {
|
|
domains = append(domains, name)
|
|
} else {
|
|
hostnames = append(hostnames, name)
|
|
}
|
|
}
|
|
|
|
sort.Strings(domains)
|
|
sort.Strings(hostnames)
|
|
|
|
ports[key] = &statusPortInfo{
|
|
Open: ps.Open,
|
|
Domains: domains,
|
|
Hostnames: hostnames,
|
|
LastChecked: ps.LastChecked,
|
|
}
|
|
}
|
|
|
|
return ports
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|