//go:build standalone package checker import ( "context" "encoding/json" "fmt" "net/http" "strconv" "strings" "github.com/miekg/dns" sdk "git.happydns.org/checker-sdk-go/checker" "git.happydns.org/checker-sdk-go/checker/server" ) // RenderForm implements server.Interactive. It exposes the minimal // inputs needed to bootstrap a standalone OPENPGPKEY/SMIMEA check: an // email address (the local part is hashed into the owner name) and a // kind selector. The DNS resolver and severity-tuning options mirror // the regular UserOpts so a human can override them on the form. func (p *emailKeyProvider) RenderForm() []sdk.CheckerOptionField { return []sdk.CheckerOptionField{ { Id: "email", Type: "string", Label: "Email address", Placeholder: "alice@example.com", Description: "Address to look up. The local part is SHA-256-hashed per RFC 7929/8162; the domain part is the zone queried.", Required: true, }, { Id: "kind", Type: "string", Label: "Record kind", Default: KindOpenPGPKey, Choices: []string{KindOpenPGPKey, KindSMIMEA}, }, { Id: OptionResolver, Type: "string", Label: "DNS resolver", Placeholder: "1.1.1.1", Description: "Validating resolver to query (comma-separated list accepted). Defaults to the system resolver when empty.", }, { Id: OptionCertExpiryWarnDays, Type: "number", Label: "Expiry warning threshold (days)", Description: "Emit a warning when the primary key or S/MIME certificate expires in less than this many days.", Default: float64(30), }, { Id: OptionRequireDNSSEC, Type: "bool", Label: "Require DNSSEC", Default: true, }, { Id: OptionRequireEmailProtection, Type: "bool", Label: "Require emailProtection EKU (SMIMEA only)", Default: true, }, } } // ParseForm implements server.Interactive. It validates the inputs, // resolves the DNS record matching the requested kind, and returns the // CheckerOptions that Collect expects, including a synthesised service // envelope built from the live DNS answer. func (p *emailKeyProvider) ParseForm(r *http.Request) (sdk.CheckerOptions, error) { email := strings.TrimSpace(r.FormValue("email")) if email == "" { return nil, fmt.Errorf("email is required") } at := strings.LastIndex(email, "@") if at <= 0 || at == len(email)-1 { return nil, fmt.Errorf("email %q must be of the form local@domain", email) } username := email[:at] domain := strings.TrimSuffix(strings.ToLower(email[at+1:]), ".") kind := strings.TrimSpace(r.FormValue("kind")) if kind == "" { kind = KindOpenPGPKey } var ( svcType string prefix string qtype uint16 ) switch kind { case KindOpenPGPKey: svcType = ServiceOpenPGP prefix = OpenPGPKeyPrefix qtype = dns.TypeOPENPGPKEY case KindSMIMEA: svcType = ServiceSMimeCert prefix = SMIMEACertPrefix qtype = dns.TypeSMIMEA default: return nil, fmt.Errorf("unknown kind %q (expected %q or %q)", kind, KindOpenPGPKey, KindSMIMEA) } resolverOpt := strings.TrimSpace(r.FormValue(OptionResolver)) owner := dns.Fqdn(ownerHashHex(username) + "." + prefix + "." + domain) ctx, cancel := context.WithTimeout(r.Context(), dnsTimeout*3) defer cancel() ans, err := lookup(ctx, resolvers(resolverOpt), owner, qtype) if err != nil { return nil, fmt.Errorf("DNS lookup for %s %s failed: %w", dns.TypeToString[qtype], owner, err) } if ans.Rcode == dns.RcodeNameError || len(ans.Records) == 0 { return nil, fmt.Errorf("no %s record found at %s", dns.TypeToString[qtype], owner) } body := serviceBody{Username: username} switch kind { case KindOpenPGPKey: rr, ok := ans.Records[0].(*dns.OPENPGPKEY) if !ok { return nil, fmt.Errorf("unexpected record type %T at %s", ans.Records[0], owner) } body.OpenPGP = rr case KindSMIMEA: rr, ok := ans.Records[0].(*dns.SMIMEA) if !ok { return nil, fmt.Errorf("unexpected record type %T at %s", ans.Records[0], owner) } body.SMIMEA = rr } bodyJSON, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("encode service body: %w", err) } svcMsg := serviceMessage{ Type: svcType, Domain: dns.Fqdn(domain), Service: bodyJSON, } opts := sdk.CheckerOptions{ "service": svcMsg, "service_type": svcType, "domain_name": domain, } if resolverOpt != "" { opts[OptionResolver] = resolverOpt } if v := strings.TrimSpace(r.FormValue(OptionCertExpiryWarnDays)); v != "" { opts[OptionCertExpiryWarnDays] = parseFloatOr(v, 30) } opts[OptionRequireDNSSEC] = r.FormValue(OptionRequireDNSSEC) == "true" opts[OptionRequireEmailProtection] = r.FormValue(OptionRequireEmailProtection) == "true" return opts, nil } // parseFloatOr parses a decimal string, returning fallback on error. func parseFloatOr(s string, fallback float64) float64 { f, err := strconv.ParseFloat(s, 64) if err != nil { return fallback } return f } // Compile-time assertion that the provider implements the optional interface. var _ server.Interactive = (*emailKeyProvider)(nil)