Files
dnstest/backend/internal/api/handler.go
robertas_stauskas a70f3262e0 Initial commit: DNS Test - DNS health checking tool
Go backend (miekg/dns) + Nuxt 3 frontend (Tailwind CSS v4).

8 check categories, 52 checks total:
- Overview: @ record, WWW, MX with ASN/provider lookup
- Domain Registration: expiry, registrar (RDAP + whois fallback)
- Parent Delegation: NS records, glue, consistency
- Nameservers: 17 checks (reachability, auth, recursion, TCP/UDP, AXFR, etc.)
- SOA: serial consistency, timing values
- Mail (MX): 11 checks (CNAME, PTR, public IPs, consistency)
- Mail Auth: SPF, DKIM, DMARC
- WWW: A record, CNAME

Features:
- SSE streaming (results appear as each category completes)
- SQLite history (modernc.org/sqlite)
- Rate limiting, CORS, request logging
- Dark mode, responsive design
2026-03-20 13:39:57 +02:00

221 lines
5.8 KiB
Go

package api
import (
"encoding/json"
"fmt"
"log"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"github.com/intodns/backend/internal/checker"
"github.com/intodns/backend/internal/store"
)
var validHostname = regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`)
func cleanDomain(raw string) string {
domain := raw
domain = strings.TrimPrefix(domain, "http://")
domain = strings.TrimPrefix(domain, "https://")
domain = strings.TrimRight(domain, "/")
if idx := strings.Index(domain, "/"); idx != -1 {
domain = domain[:idx]
}
if idx := strings.LastIndex(domain, ":"); idx != -1 {
domain = domain[:idx]
}
return strings.ToLower(strings.TrimSpace(domain))
}
// CheckHandler returns the full report as JSON (non-streaming).
func CheckHandler(ch *checker.Checker, st *store.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
domain := cleanDomain(r.URL.Query().Get("domain"))
if domain == "" {
writeJSON(w, http.StatusBadRequest, ErrorResponse{Error: "missing 'domain' query parameter"})
return
}
if !validHostname.MatchString(domain) {
writeJSON(w, http.StatusBadRequest, ErrorResponse{Error: "invalid domain name"})
return
}
report := ch.Check(domain)
if st != nil {
if err := st.SaveReport(report, extractIP(r), r.UserAgent()); err != nil {
log.Printf("failed to save report: %v", err)
}
}
writeJSON(w, http.StatusOK, report)
}
}
// CheckStreamHandler streams results via Server-Sent Events as each category completes.
func CheckStreamHandler(ch *checker.Checker, st *store.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
domain := cleanDomain(r.URL.Query().Get("domain"))
if domain == "" {
writeJSON(w, http.StatusBadRequest, ErrorResponse{Error: "missing 'domain' query parameter"})
return
}
if !validHostname.MatchString(domain) {
writeJSON(w, http.StatusBadRequest, ErrorResponse{Error: "invalid domain name"})
return
}
flusher, ok := w.(http.Flusher)
if !ok {
// Fallback to non-streaming.
report := ch.Check(domain)
if st != nil {
if err := st.SaveReport(report, extractIP(r), r.UserAgent()); err != nil {
log.Printf("failed to save report: %v", err)
}
}
writeJSON(w, http.StatusOK, report)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Accel-Buffering", "no")
w.WriteHeader(http.StatusOK)
flusher.Flush()
start := time.Now()
catCh := ch.CheckStream(domain)
var categories []checker.Category
for event := range catCh {
categories = append(categories, event.Category)
data, err := json.Marshal(event)
if err != nil {
continue
}
fmt.Fprintf(w, "data: %s\n\n", data)
flusher.Flush()
}
// Build and save the final report.
if st != nil {
var summary checker.Summary
for _, cat := range categories {
for _, check := range cat.Checks {
switch check.Status {
case checker.StatusPass:
summary.Pass++
case checker.StatusWarn:
summary.Warn++
case checker.StatusFail:
summary.Fail++
case checker.StatusInfo:
summary.Info++
}
}
}
report := &checker.Report{
Domain: domain,
Timestamp: time.Now().UTC().Format(time.RFC3339),
DurationMs: time.Since(start).Milliseconds(),
Summary: summary,
Categories: categories,
}
if err := st.SaveReport(report, extractIP(r), r.UserAgent()); err != nil {
log.Printf("failed to save streamed report: %v", err)
}
}
fmt.Fprintf(w, "data: {\"done\":true}\n\n")
flusher.Flush()
}
}
// HistoryHandler returns recent check history as JSON.
// Query params: domain (optional), limit (default 20, max 100).
func HistoryHandler(st *store.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
domain := r.URL.Query().Get("domain")
limitStr := r.URL.Query().Get("limit")
limit := 20
if limitStr != "" {
if n, err := strconv.Atoi(limitStr); err == nil && n > 0 {
limit = n
}
}
if limit > 100 {
limit = 100
}
var entries []store.HistoryEntry
var err error
if domain != "" {
entries, err = st.GetHistory(domain, limit)
} else {
entries, err = st.GetRecent(limit)
}
if err != nil {
log.Printf("history query error: %v", err)
writeJSON(w, http.StatusInternalServerError, ErrorResponse{Error: "failed to query history"})
return
}
if entries == nil {
entries = []store.HistoryEntry{}
}
writeJSON(w, http.StatusOK, entries)
}
}
// ReportHandler returns the full saved report JSON for a specific history entry.
// Query param: id (required).
func ReportHandler(st *store.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Query().Get("id")
if idStr == "" {
writeJSON(w, http.StatusBadRequest, ErrorResponse{Error: "missing 'id' query parameter"})
return
}
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
writeJSON(w, http.StatusBadRequest, ErrorResponse{Error: "invalid 'id' parameter"})
return
}
entry, err := st.GetReport(id)
if err != nil {
log.Printf("report query error: %v", err)
writeJSON(w, http.StatusInternalServerError, ErrorResponse{Error: "failed to query report"})
return
}
if entry == nil {
writeJSON(w, http.StatusNotFound, ErrorResponse{Error: "report not found"})
return
}
writeJSON(w, http.StatusOK, entry)
}
}
// HealthHandler returns an http.HandlerFunc for the health endpoint.
func HealthHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, HealthResponse{Status: "ok"})
}
}
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}