// Package acme turns "publish (or remove) this DNS-01 challenge value" // into record changes. The jarl CLI (jarl acme) and jarld's httpreq // endpoint for lego both use it, so they behave the same way. package acme import ( "encoding/json" "strings" "jarl/internal/api" "jarl/internal/dnsname" ) // DefaultTTL is the TTL of challenge records. const DefaultTTL = 60 // Comment is stored with every challenge record. const Comment = "ACME DNS-01 challenge" // Zone is a primary zone that may receive challenge records. type Zone struct { Name string // as Jarl stores it, e.g. "adbraun.lol." View string // "" when views are not used } // Zones returns the zones that hold name: the longest matching zone, in // every view that has it, or only in view when view is set. The record is // written into every view so that the CA (usually in an external view) and // internal resolvers see the same thing. func Zones(name string, zones []Zone, view string) []Zone { name = dnsname.Fqdn(name) best := "" var out []Zone for _, z := range zones { if view != "" && !strings.EqualFold(z.View, view) { continue } zn := dnsname.Fqdn(z.Name) if !dnsname.IsSubdomain(name, zn) { continue } switch { case len(zn) > len(best): best, out = zn, []Zone{z} case zn == best: out = append(out, z) } } return out } // Requests returns the changes that set (or, with set false, remove) the // TXT values at name in each zone. Setting first deletes the same value, so // a retried hook succeeds instead of failing on a duplicate record, and // removing a value that isn't there succeeds too. Removing with no values // removes every TXT record at name. func Requests(name string, zones []Zone, values []string, set bool, ttl uint32) []api.ChangeRequest { name = dnsname.Fqdn(name) var reqs []api.ChangeRequest add := func(op string, z Zone, extra map[string]any) { m := map[string]any{"zone": z.Name, "name": name, "type": "TXT"} if z.View != "" { m["view"] = z.View } for k, v := range extra { m[k] = v } b, _ := json.Marshal(m) reqs = append(reqs, api.ChangeRequest{Op: op, Data: b}) } for _, z := range zones { if !set && len(values) == 0 { add("record.delete", z, map[string]any{"missing_ok": true}) continue } for _, v := range values { add("record.delete", z, map[string]any{"data": v, "missing_ok": true}) if set { add("record.create", z, map[string]any{"data": v, "ttl": ttl, "comment": Comment}) } } } return reqs } // Description is the changeset description for a change at name. func Description(name string, set bool) string { verb := "remove" if set { verb = "set" } return "ACME: " + verb + " challenge for " + dnsname.Display(dnsname.Fqdn(name)) }