138 lines
4 KiB
Go
138 lines
4 KiB
Go
package checker
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
// Collect walks the working zone and records every legacy RR encountered.
|
|
// We decode the zone as a minimal local shape (rawZone) so the checker stays
|
|
// free of any happyDomain module dependency. Almost every legacy record
|
|
// reaches us as an "svcs.Orphan" (happyDomain has no dedicated service for
|
|
// these types), so the orphan body is the primary path; other service types
|
|
// are also probed for an embedded RR header on a best-effort basis.
|
|
func (p *legacyProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) {
|
|
zone, err := readZone(opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data := &LegacyData{Zone: zone.DomainName}
|
|
if data.Zone == "" {
|
|
if name, ok := sdk.GetOption[string](opts, "domain_name"); ok {
|
|
data.Zone = strings.TrimSuffix(name, ".")
|
|
}
|
|
}
|
|
|
|
// Sort subdomains so the report ordering is stable across runs and
|
|
// findings stay diff-friendly when the user replays the check.
|
|
subs := make([]string, 0, len(zone.Services))
|
|
for s := range zone.Services {
|
|
subs = append(subs, s)
|
|
}
|
|
sort.Strings(subs)
|
|
|
|
for _, sub := range subs {
|
|
for _, svc := range zone.Services[sub] {
|
|
data.ServicesScanned++
|
|
f, perr := inspectService(sub, svc)
|
|
if perr != nil {
|
|
data.CollectErrors = append(data.CollectErrors,
|
|
fmt.Sprintf("%s/%s: %v", displaySubdomain(sub), svc.Type, perr))
|
|
continue
|
|
}
|
|
data.Findings = append(data.Findings, f...)
|
|
}
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// readZone normalises the "zone" option, which arrives either as a native
|
|
// *Zone (in-process plugin) or as a JSON object (HTTP path). We round-trip
|
|
// through json.Marshal in both cases: it costs one allocation and keeps the
|
|
// rawZone decoder as the single shape contract.
|
|
func readZone(opts sdk.CheckerOptions) (*rawZone, error) {
|
|
v, ok := opts["zone"]
|
|
if !ok || v == nil {
|
|
return nil, fmt.Errorf("missing 'zone' option (AutoFillZone): the host did not provide a working zone")
|
|
}
|
|
raw, err := json.Marshal(v)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("re-marshal zone option: %w", err)
|
|
}
|
|
z := &rawZone{}
|
|
if err := json.Unmarshal(raw, z); err != nil {
|
|
return nil, fmt.Errorf("decode zone option: %w", err)
|
|
}
|
|
return z, nil
|
|
}
|
|
|
|
// inspectService returns one finding per legacy record carried by the
|
|
// service. Returns (nil, nil) for non-legacy services (the common case).
|
|
func inspectService(sub string, svc rawService) ([]Finding, error) {
|
|
hdr, ok, err := extractRRHeader(svc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
if _, deprecated := deprecatedTypes[hdr.Rrtype]; !deprecated {
|
|
return nil, nil
|
|
}
|
|
|
|
return []Finding{{
|
|
Subdomain: sub,
|
|
Name: hdr.Name,
|
|
Rrtype: hdr.Rrtype,
|
|
TypeName: typeLabel(hdr.Rrtype),
|
|
ServiceType: svc.Type,
|
|
}}, nil
|
|
}
|
|
|
|
// extractRRHeader pulls the RR header from a service body. Only svcs.Orphan
|
|
// exposes such a header on the wire today; other service types are skipped
|
|
// silently so the common case (MX, A, TXT, …) does not pollute CollectErrors.
|
|
// When the service *is* an orphan but the body fails to decode, the error is
|
|
// propagated so the operator sees the malformed entry in the report.
|
|
func extractRRHeader(svc rawService) (orphanHdr, bool, error) {
|
|
if len(svc.Service) == 0 {
|
|
return orphanHdr{}, false, nil
|
|
}
|
|
|
|
if svc.Type != "svcs.Orphan" {
|
|
return orphanHdr{}, false, nil
|
|
}
|
|
|
|
var ob orphanBody
|
|
if err := json.Unmarshal(svc.Service, &ob); err != nil {
|
|
return orphanHdr{}, false, fmt.Errorf("decode orphan body: %w", err)
|
|
}
|
|
if ob.Record.Hdr.Rrtype == 0 {
|
|
return orphanHdr{}, false, nil
|
|
}
|
|
return orphanHdr(ob.Record.Hdr), true, nil
|
|
}
|
|
|
|
// orphanHdr is a flat copy of orphanBody.Record.Hdr so callers don't have
|
|
// to know about the JSON nesting.
|
|
type orphanHdr struct {
|
|
Name string `json:"Name"`
|
|
Rrtype uint16 `json:"Rrtype"`
|
|
}
|
|
|
|
// displaySubdomain renders the apex as "@" so error messages match the
|
|
// convention used everywhere else in happyDomain.
|
|
func displaySubdomain(s string) string {
|
|
if s == "" || s == "@" {
|
|
return "@"
|
|
}
|
|
return s
|
|
}
|