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 minimal local copy of happydns.ServiceMessage // matching the JSON wire shape, so this module does not depend on the // happyDomain core repository. Same pattern as // checker-ns-restrictions/checker/types.go. type serviceMessage struct { Type string `json:"_svctype"` Domain string `json:"_domain"` Service json.RawMessage `json:"Service"` } // caaPolicyPayload mirrors the JSON shape of svcs.CAAPolicy: a single // "caa" field holding a list of CAA records. Each record is decoded // into a trimmed local type with just the fields the rule reads. 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 pulls the "service" option out of the options map, // accepting both the in-process plugin path (native Go value) and the // HTTP path (JSON-decoded map[string]any). Normalizing via a JSON // round-trip keeps both paths working 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 }