checker-caa/checker/collect.go

89 lines
2.5 KiB
Go

package checker
import (
"context"
"encoding/json"
"fmt"
"time"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// serviceType is the happyDomain service type string this checker binds to.
const serviceType = "svcs.CAAPolicy"
// serviceMessage is a local copy of happydns.ServiceMessage to avoid
// depending on the happyDomain core repository.
type serviceMessage struct {
Type string `json:"_svctype"`
Domain string `json:"_domain"`
Service json.RawMessage `json:"Service"`
}
type caaPolicyPayload struct {
Records []caaRecordPayload `json:"caa"`
}
// caaRecordPayload matches miekg/dns.CAA's JSON tags
// (Hdr/Flag/Tag/Value) closely enough to round-trip through the
// service body. We only keep Flag/Tag/Value; the Hdr is ignored.
type caaRecordPayload struct {
Flag uint8 `json:"Flag"`
Tag string `json:"Tag"`
Value string `json:"Value"`
}
// Collect reads the auto-filled service body, validates the type, and
// returns the CAA records flattened into CAAData. No network call.
func (p *caaProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) {
svc, err := serviceFromOptions(opts)
if err != nil {
return nil, err
}
if svc.Type != serviceType {
return nil, fmt.Errorf("service is %q, expected %q", svc.Type, serviceType)
}
var pol caaPolicyPayload
if err := json.Unmarshal(svc.Service, &pol); err != nil {
return nil, fmt.Errorf("decode CAA policy: %w", err)
}
records := make([]CAARecord, 0, len(pol.Records))
for _, r := range pol.Records {
records = append(records, CAARecord{Flag: r.Flag, Tag: r.Tag, Value: r.Value})
}
domain := svc.Domain
if domain == "" {
if v, _ := sdk.GetOption[string](opts, "domain"); v != "" {
domain = v
}
}
return &CAAData{
Domain: domain,
Records: records,
RunAt: time.Now().UTC().Format(time.RFC3339),
}, nil
}
// serviceFromOptions normalizes the "service" option via a JSON
// round-trip so the in-process plugin path (native Go value) and the
// HTTP path (decoded map[string]any) both work without importing the
// upstream type.
func serviceFromOptions(opts sdk.CheckerOptions) (*serviceMessage, error) {
v, ok := opts["service"]
if !ok {
return nil, fmt.Errorf("service option missing")
}
raw, err := json.Marshal(v)
if err != nil {
return nil, fmt.Errorf("marshal service option: %w", err)
}
var svc serviceMessage
if err := json.Unmarshal(raw, &svc); err != nil {
return nil, fmt.Errorf("decode service option: %w", err)
}
return &svc, nil
}