Initial commit
This commit is contained in:
commit
19296f4188
18 changed files with 2562 additions and 0 deletions
171
checker/interactive.go
Normal file
171
checker/interactive.go
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// RenderForm implements sdk.CheckerInteractive. 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 sdk.CheckerInteractive. 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) + "." + strings.TrimPrefix(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 {
|
||||
var f float64
|
||||
if _, err := fmt.Sscanf(s, "%f", &f); err != nil {
|
||||
return fallback
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// Compile-time assertion that the provider implements the optional interface.
|
||||
var _ sdk.CheckerInteractive = (*emailKeyProvider)(nil)
|
||||
Loading…
Add table
Add a link
Reference in a new issue