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 }