diff options
Diffstat (limited to 'plugin/pkg/replacer/replacer.go')
-rw-r--r-- | plugin/pkg/replacer/replacer.go | 211 |
1 files changed, 112 insertions, 99 deletions
diff --git a/plugin/pkg/replacer/replacer.go b/plugin/pkg/replacer/replacer.go index 8c8032c76..f72e1ec9c 100644 --- a/plugin/pkg/replacer/replacer.go +++ b/plugin/pkg/replacer/replacer.go @@ -13,123 +13,134 @@ import ( "github.com/miekg/dns" ) -// Replacer is a type which can replace placeholder -// substrings in a string with actual values from a -// dns.Msg and responseRecorder. Always use -// NewReplacer to get one of these. -type Replacer interface { - Replace(string) string - Set(key, value string) +// Replacer replaces labels for values in strings. +type Replacer struct { + valueFunc func(request.Request, *dnstest.Recorder, string) string + labels []string } -type replacer struct { - ctx context.Context - replacements map[string]string - emptyValue string +// labels are all supported labels that can be used in the default Replacer. +var labels = []string{ + "{type}", + "{name}", + "{class}", + "{proto}", + "{size}", + "{remote}", + "{port}", + "{local}", + // Header values. + headerReplacer + "id}", + headerReplacer + "opcode}", + headerReplacer + "do}", + headerReplacer + "bufsize}", + // Recorded replacements. + "{rcode}", + "{rsize}", + "{duration}", + headerReplacer + "rrflags}", } -// New makes a new replacer based on r and rr. -// Do not create a new replacer until r and rr have all -// the needed values, because this function copies those -// values into the replacer. rr may be nil if it is not -// available. emptyValue should be the string that is used -// in place of empty string (can still be empty string). -func New(ctx context.Context, r *dns.Msg, rr *dnstest.Recorder, emptyValue string) Replacer { - req := request.Request{W: rr, Req: r} - rep := replacer{ - ctx: ctx, - replacements: map[string]string{ - "{type}": req.Type(), - "{name}": req.Name(), - "{class}": req.Class(), - "{proto}": req.Proto(), - "{when}": "", // made a noop - "{size}": strconv.Itoa(req.Len()), - "{remote}": addrToRFC3986(req.IP()), - "{port}": req.Port(), - "{local}": addrToRFC3986(req.LocalIP()), - }, - emptyValue: emptyValue, - } - if rr != nil { +// value returns the current value of label. +func value(state request.Request, rr *dnstest.Recorder, label string) string { + switch label { + case "{type}": + return state.Type() + case "{name}": + return state.Name() + case "{class}": + return state.Class() + case "{proto}": + return state.Proto() + case "{size}": + return strconv.Itoa(state.Req.Len()) + case "{remote}": + return addrToRFC3986(state.IP()) + case "{port}": + return state.Port() + case "{local}": + return addrToRFC3986(state.LocalIP()) + // Header placeholders (case-insensitive). + case headerReplacer + "id}": + return strconv.Itoa(int(state.Req.Id)) + case headerReplacer + "opcode}": + return strconv.Itoa(state.Req.Opcode) + case headerReplacer + "do}": + return boolToString(state.Do()) + case headerReplacer + "bufsize}": + return strconv.Itoa(state.Size()) + // Recorded replacements. + case "{rcode}": + if rr == nil { + return EmptyValue + } rcode := dns.RcodeToString[rr.Rcode] if rcode == "" { rcode = strconv.Itoa(rr.Rcode) } - rep.replacements["{rcode}"] = rcode - rep.replacements["{rsize}"] = strconv.Itoa(rr.Len) - rep.replacements["{duration}"] = strconv.FormatFloat(time.Since(rr.Start).Seconds(), 'f', -1, 64) + "s" - if rr.Msg != nil { - rep.replacements[headerReplacer+"rflags}"] = flagsToString(rr.Msg.MsgHdr) + return rcode + case "{rsize}": + if rr == nil { + return EmptyValue + } + return strconv.Itoa(rr.Len) + case "{duration}": + if rr == nil { + return EmptyValue + } + return strconv.FormatFloat(time.Since(rr.Start).Seconds(), 'f', -1, 64) + "s" + case headerReplacer + "rrflags}": + if rr != nil && rr.Msg != nil { + return flagsToString(rr.Msg.MsgHdr) } + return EmptyValue } + return EmptyValue +} - // Header placeholders (case-insensitive) - rep.replacements[headerReplacer+"id}"] = strconv.Itoa(int(r.Id)) - rep.replacements[headerReplacer+"opcode}"] = strconv.Itoa(r.Opcode) - rep.replacements[headerReplacer+"do}"] = boolToString(req.Do()) - rep.replacements[headerReplacer+"bufsize}"] = strconv.Itoa(req.Size()) - - return rep +// New makes a new replacer. This only needs to be called once in the setup and then call Replace for each incoming message. +// A replacer is safe for concurrent use. +func New() Replacer { + return Replacer{ + valueFunc: value, + labels: labels, + } } -// Replace performs a replacement of values on s and returns -// the string with the replaced values. -func (r replacer) Replace(s string) string { - - // declare a function that replace based on header matching - fscanAndReplace := func(s string, header string, replace func(string) string) string { - b := strings.Builder{} - for strings.Contains(s, header) { - idxStart := strings.Index(s, header) - endOffset := idxStart + len(header) - idxEnd := strings.Index(s[endOffset:], "}") - if idxEnd > -1 { - placeholder := strings.ToLower(s[idxStart : endOffset+idxEnd+1]) - replacement := replace(placeholder) - if replacement == "" { - replacement = r.emptyValue - } - b.WriteString(s[:idxStart]) - b.WriteString(replacement) - s = s[endOffset+idxEnd+1:] - } else { - break - } +// Replace performs a replacement of values on s and returns the string with the replaced values. +func (r Replacer) Replace(ctx context.Context, state request.Request, rr *dnstest.Recorder, s string) string { + for _, placeholder := range r.labels { + if strings.Contains(s, placeholder) { + s = strings.Replace(s, placeholder, r.valueFunc(state, rr, placeholder), -1) } - b.WriteString(s) - return b.String() } - // Header replacements - these are case-insensitive, so we can't just use strings.Replace() - s = fscanAndReplace(s, headerReplacer, func(placeholder string) string { - return r.replacements[placeholder] - }) + // Metadata label replacements. Scan for {/ and search for next }, replace that metadata label with + // any meta data that is available. + b := strings.Builder{} + for strings.Contains(s, labelReplacer) { + idxStart := strings.Index(s, labelReplacer) + endOffset := idxStart + len(labelReplacer) + idxEnd := strings.Index(s[endOffset:], "}") + if idxEnd > -1 { + label := s[idxStart+2 : endOffset+idxEnd] + + fm := metadata.ValueFunc(ctx, label) + replacement := EmptyValue + if fm != nil { + replacement = fm() + } - // Regular replacements - these are easier because they're case-sensitive - for placeholder, replacement := range r.replacements { - if replacement == "" { - replacement = r.emptyValue + b.WriteString(s[:idxStart]) + b.WriteString(replacement) + s = s[endOffset+idxEnd+1:] + } else { + break } - s = strings.Replace(s, placeholder, replacement, -1) } - // Metadata label replacements - s = fscanAndReplace(s, headerLabelReplacer, func(placeholder string) string { - // label place holder has the format {/<label>} - fm := metadata.ValueFunc(r.ctx, placeholder[len(headerLabelReplacer):len(placeholder)-1]) - if fm != nil { - return fm() - } - return "" - }) - - return s -} - -// Set sets key to value in the replacements map. -func (r replacer) Set(key, value string) { - r.replacements["{"+key+"}"] = value + b.WriteString(s) + return b.String() } func boolToString(b bool) string { @@ -190,6 +201,8 @@ func addrToRFC3986(addr string) string { } const ( - headerReplacer = "{>" - headerLabelReplacer = "{/" + headerReplacer = "{>" + labelReplacer = "{/" + // EmptyValue is the default empty value. + EmptyValue = "-" ) |