diff --git a/internal/handlers/status.go b/internal/handlers/status.go index 9996fc8..b01fd6b 100644 --- a/internal/handlers/status.go +++ b/internal/handlers/status.go @@ -2,22 +2,217 @@ package handlers import ( "net/http" + "sort" + "time" + + "sneak.berlin/go/dnswatcher/internal/state" ) +// statusDomainInfo holds status information for a monitored domain. +type statusDomainInfo struct { + Nameservers []string `json:"nameservers"` + 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"` + 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"` + 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 { - type response struct { - Status string `json:"status"` - } - return func( writer http.ResponseWriter, request *http.Request, ) { + snap := h.state.GetSnapshot() + + resp := buildStatusResponse(snap) + h.respondJSON( writer, request, - &response{Status: "ok"}, + 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), + } + + buildDomains(snap, resp) + buildHostnames(snap, resp) + buildPorts(snap, resp) + buildCertificates(snap, resp) + buildCounts(resp) + + return resp +} + +func buildDomains( + snap state.Snapshot, + resp *statusResponse, +) { + for name, ds := range snap.Domains { + ns := make([]string, len(ds.Nameservers)) + copy(ns, ds.Nameservers) + sort.Strings(ns) + + resp.Domains[name] = &statusDomainInfo{ + Nameservers: ns, + LastChecked: ds.LastChecked, + } + } +} + +func buildHostnames( + snap state.Snapshot, + resp *statusResponse, +) { + for name, hs := range snap.Hostnames { + info := &statusHostnameInfo{ + Nameservers: make(map[string]*statusHostnameNSInfo), + LastChecked: hs.LastChecked, + } + + 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.Nameservers[ns] = &statusHostnameNSInfo{ + Records: recs, + Status: nsState.Status, + LastChecked: nsState.LastChecked, + } + } + + resp.Hostnames[name] = 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, + 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, + } +}