89 lines
2.5 KiB
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
|
|
}
|