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
146 lines
2.8 KiB
Go
146 lines
2.8 KiB
Go
package checker
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/intodns/backend/internal/resolver"
|
|
)
|
|
|
|
// Checker orchestrates all DNS health checks for a domain.
|
|
type Checker struct {
|
|
resolver *resolver.Resolver
|
|
}
|
|
|
|
// NewChecker creates a Checker with a default Resolver.
|
|
func NewChecker() *Checker {
|
|
return &Checker{
|
|
resolver: resolver.NewResolver(),
|
|
}
|
|
}
|
|
|
|
// StreamEvent is sent for each completed category during streaming.
|
|
type StreamEvent struct {
|
|
Domain string `json:"domain,omitempty"`
|
|
Category Category `json:"category"`
|
|
Done bool `json:"done,omitempty"`
|
|
}
|
|
|
|
// Check runs all category checks in parallel and returns a Report.
|
|
func (c *Checker) Check(domain string) *Report {
|
|
start := time.Now()
|
|
|
|
type catResult struct {
|
|
index int
|
|
cat Category
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
results := make(chan catResult, 8)
|
|
|
|
categories := c.categoryFuncs()
|
|
|
|
for _, cf := range categories {
|
|
wg.Add(1)
|
|
go func(idx int, fn func(string, *resolver.Resolver) Category) {
|
|
defer wg.Done()
|
|
cat := fn(domain, c.resolver)
|
|
results <- catResult{index: idx, cat: cat}
|
|
}(cf.index, cf.fn)
|
|
}
|
|
|
|
go func() {
|
|
wg.Wait()
|
|
close(results)
|
|
}()
|
|
|
|
ordered := make([]Category, len(categories))
|
|
for cr := range results {
|
|
ordered[cr.index] = cr.cat
|
|
}
|
|
|
|
var summary Summary
|
|
for _, cat := range ordered {
|
|
for _, check := range cat.Checks {
|
|
switch check.Status {
|
|
case StatusPass:
|
|
summary.Pass++
|
|
case StatusWarn:
|
|
summary.Warn++
|
|
case StatusFail:
|
|
summary.Fail++
|
|
case StatusInfo:
|
|
summary.Info++
|
|
}
|
|
}
|
|
}
|
|
|
|
return &Report{
|
|
Domain: domain,
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
|
DurationMs: time.Since(start).Milliseconds(),
|
|
Summary: summary,
|
|
Categories: ordered,
|
|
}
|
|
}
|
|
|
|
// CheckStream runs all categories in parallel and sends each category as it completes.
|
|
func (c *Checker) CheckStream(domain string) <-chan StreamEvent {
|
|
out := make(chan StreamEvent, 6)
|
|
|
|
go func() {
|
|
defer close(out)
|
|
|
|
type catResult struct {
|
|
index int
|
|
cat Category
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
results := make(chan catResult, 8)
|
|
|
|
categories := c.categoryFuncs()
|
|
|
|
for _, cf := range categories {
|
|
wg.Add(1)
|
|
go func(idx int, fn func(string, *resolver.Resolver) Category) {
|
|
defer wg.Done()
|
|
cat := fn(domain, c.resolver)
|
|
results <- catResult{index: idx, cat: cat}
|
|
}(cf.index, cf.fn)
|
|
}
|
|
|
|
go func() {
|
|
wg.Wait()
|
|
close(results)
|
|
}()
|
|
|
|
for cr := range results {
|
|
out <- StreamEvent{
|
|
Domain: domain,
|
|
Category: cr.cat,
|
|
}
|
|
}
|
|
}()
|
|
|
|
return out
|
|
}
|
|
|
|
type catFunc struct {
|
|
index int
|
|
fn func(string, *resolver.Resolver) Category
|
|
}
|
|
|
|
func (c *Checker) categoryFuncs() []catFunc {
|
|
return []catFunc{
|
|
{0, checkOverview},
|
|
{1, checkDomainWhois},
|
|
{2, checkParent},
|
|
{3, checkNS},
|
|
{4, checkSOA},
|
|
{5, checkMX},
|
|
{6, checkMail},
|
|
{7, checkWWW},
|
|
}
|
|
}
|