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. // 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, 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, } }