91f7a02f2c
Parameterize used strings.ReplaceAll, so an external host that merely
ends with the zone name as a substring (e.g. "notreconops.ru." against
zone "reconops.ru") was falsely rewritten to "not{{domain_name}}.".
Replace only where the zone name sits on a DNS-label boundary (start/
end of string or a non-alphanumeric/hyphen character), and resolve to
a fixed point so adjacent occurrences sharing a single boundary
character are still both replaced.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
97 lines
3.6 KiB
Go
97 lines
3.6 KiB
Go
// Package tmpl renders reusable DNS templates for a concrete zone.
|
|
//
|
|
// A template stores records containing the {{domain_name}} placeholder; it is
|
|
// materialised (placeholder -> zone name, without the trailing dot) just
|
|
// before diff/apply against a specific domain, and parameterised (zone name
|
|
// -> placeholder) when snapshotting a live zone into a reusable template.
|
|
package tmpl
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/vasyakrg/dns-autoresolver/internal/model"
|
|
"github.com/vasyakrg/dns-autoresolver/internal/store/dto"
|
|
)
|
|
|
|
const Placeholder = "{{domain_name}}"
|
|
|
|
func zoneName(z string) string { return strings.TrimSuffix(z, ".") }
|
|
|
|
// replaceZoneBoundary replaces occurrences of the zone name matched by re
|
|
// with Placeholder, but only where the match sits on a DNS-label boundary:
|
|
// the zone name must not be a suffix of a longer label. re must be built
|
|
// from zoneBoundaryPattern for a specific zone name. A boundary is the
|
|
// start/end of the string, or any character that cannot appear inside a DNS
|
|
// label (i.e. not a letter, digit, or hyphen — a dot qualifies). This
|
|
// prevents an external host that merely ends with the zone name as a
|
|
// substring (e.g. "notreconops.ru." against zone "reconops.ru") from being
|
|
// falsely parameterized.
|
|
//
|
|
// Replacement runs to a fixed point: a single regexp pass is non-overlapping,
|
|
// so two adjacent zone occurrences separated by a single boundary character
|
|
// (e.g. "reconops.ru.reconops.ru.") would only have the first one matched,
|
|
// since that shared separator is consumed as the first match's right
|
|
// boundary and is unavailable as the second match's left boundary. Placeholder
|
|
// ends in "}", itself a valid boundary character, so re-running the pass
|
|
// against the previous pass's output resolves this without double-counting.
|
|
func replaceZoneBoundary(s string, re *regexp.Regexp) string {
|
|
for {
|
|
next := re.ReplaceAllString(s, "${1}"+Placeholder+"${2}")
|
|
if next == s {
|
|
return s
|
|
}
|
|
s = next
|
|
}
|
|
}
|
|
|
|
// zoneBoundaryPattern builds the regexp source used to find z on a
|
|
// DNS-label boundary within a record's Name or Value.
|
|
func zoneBoundaryPattern(z string) string {
|
|
return `(^|[^a-zA-Z0-9-])` + regexp.QuoteMeta(z) + `([^a-zA-Z0-9-]|$)`
|
|
}
|
|
|
|
// Materialize renders a template's records for the given zone, substituting
|
|
// {{domain_name}} with the zone name (without the trailing dot) in each
|
|
// record's Name and every Value.
|
|
func Materialize(doc dto.TemplateDoc, zone string) []model.Record {
|
|
z := zoneName(zone)
|
|
out := make([]model.Record, 0, len(doc.Records))
|
|
for _, r := range doc.Records {
|
|
vals := make([]string, len(r.Values))
|
|
for i, v := range r.Values {
|
|
vals[i] = strings.ReplaceAll(v, Placeholder, z)
|
|
}
|
|
out = append(out, model.Record{
|
|
Type: model.RecordType(r.Type),
|
|
Name: strings.ReplaceAll(r.Name, Placeholder, z),
|
|
TTL: r.TTL,
|
|
Values: vals,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Parameterize replaces occurrences of the zone name with {{domain_name}} in
|
|
// record names and values, so a snapshot of a live zone becomes portable.
|
|
// Records with no zone-name occurrence (DKIM key, external CNAME target) are
|
|
// left unchanged.
|
|
func Parameterize(recs []model.Record, zone string) dto.TemplateDoc {
|
|
z := zoneName(zone)
|
|
re := regexp.MustCompile(zoneBoundaryPattern(z))
|
|
out := dto.TemplateDoc{Records: make([]dto.RecordDTO, 0, len(recs))}
|
|
for _, r := range recs {
|
|
vals := make([]string, len(r.Values))
|
|
for i, v := range r.Values {
|
|
vals[i] = replaceZoneBoundary(v, re)
|
|
}
|
|
out.Records = append(out.Records, dto.RecordDTO{
|
|
Type: string(r.Type),
|
|
Name: replaceZoneBoundary(r.Name, re),
|
|
TTL: r.TTL,
|
|
Values: vals,
|
|
})
|
|
}
|
|
return out
|
|
}
|