diff --git a/checker/interactive.go b/checker/interactive.go new file mode 100644 index 0000000..d7de7c8 --- /dev/null +++ b/checker/interactive.go @@ -0,0 +1,103 @@ +package checker + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + sdk "git.happydns.org/checker-sdk-go/checker" + "github.com/miekg/dns" +) + +// RenderForm implements sdk.CheckerInteractive. It lists the minimal human +// inputs needed to bootstrap a check when this checker runs standalone +// (outside of a happyDomain host). +func (p *nsProvider) RenderForm() []sdk.CheckerOptionField { + return []sdk.CheckerOptionField{ + { + Id: "domain", + Type: "string", + Label: "Domain name", + Placeholder: "example.com", + Required: true, + Description: "Zone to probe. Its NS records will be resolved and each nameserver tested.", + }, + } +} + +// ParseForm implements sdk.CheckerInteractive. It resolves the NS records +// for the requested domain via DNS and assembles the CheckerOptions that +// Collect expects — replacing the AutoFill work that happyDomain would +// otherwise perform. +func (p *nsProvider) ParseForm(r *http.Request) (sdk.CheckerOptions, error) { + domain := strings.TrimSpace(r.FormValue("domain")) + if domain == "" { + return nil, errors.New("domain is required") + } + fqdn := dns.Fqdn(domain) + + nsRecords, err := resolveNS(fqdn) + if err != nil { + return nil, fmt.Errorf("could not resolve NS records for %s: %w", domain, err) + } + if len(nsRecords) == 0 { + return nil, fmt.Errorf("no NS records found for %s", domain) + } + + payload, err := json.Marshal(originPayload{NameServers: nsRecords}) + if err != nil { + return nil, fmt.Errorf("failed to encode origin payload: %w", err) + } + + svc := serviceMessage{ + Type: serviceTypeOrigin, + Domain: "", + Service: payload, + } + + return sdk.CheckerOptions{ + "service": svc, + "domainName": strings.TrimSuffix(fqdn, "."), + }, nil +} + +// resolveNS queries the system resolver for the NS records of fqdn and +// returns them as miekg *dns.NS records so they match the shape produced +// by happyDomain's Origin service payload. +func resolveNS(fqdn string) ([]*dns.NS, error) { + c := new(dns.Client) + m := new(dns.Msg) + m.SetQuestion(fqdn, dns.TypeNS) + m.RecursionDesired = true + + config, err := dns.ClientConfigFromFile("/etc/resolv.conf") + if err != nil || config == nil || len(config.Servers) == 0 { + config = &dns.ClientConfig{Servers: []string{"1.1.1.1", "8.8.8.8"}, Port: "53"} + } + + var lastErr error + for _, server := range config.Servers { + in, _, err := c.Exchange(m, server+":"+config.Port) + if err != nil { + lastErr = err + continue + } + if in.Rcode != dns.RcodeSuccess { + lastErr = fmt.Errorf("DNS response code %s", dns.RcodeToString[in.Rcode]) + continue + } + var records []*dns.NS + for _, rr := range in.Answer { + if ns, ok := rr.(*dns.NS); ok { + records = append(records, ns) + } + } + return records, nil + } + if lastErr == nil { + lastErr = errors.New("no resolver available") + } + return nil, lastErr +}