coredns/plugin/pkg/replacer/replacer.go
2022-07-10 11:06:33 -07:00

277 lines
6.7 KiB
Go

package replacer
import (
"context"
"strconv"
"strings"
"sync"
"time"
"github.com/coredns/coredns/plugin/metadata"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
// Replacer replaces labels for values in strings.
type Replacer struct{}
// 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{}
}
// 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 {
return loadFormat(s).Replace(ctx, state, rr)
}
const (
headerReplacer = "{>"
// EmptyValue is the default empty value.
EmptyValue = "-"
)
// labels are all supported labels that can be used in the default Replacer.
var labels = map[string]struct{}{
"{type}": {},
"{name}": {},
"{class}": {},
"{proto}": {},
"{size}": {},
"{remote}": {},
"{port}": {},
"{local}": {},
// Header values.
headerReplacer + "id}": {},
headerReplacer + "opcode}": {},
headerReplacer + "do}": {},
headerReplacer + "bufsize}": {},
// Recorded replacements.
"{rcode}": {},
"{rsize}": {},
"{duration}": {},
headerReplacer + "rflags}": {},
}
// appendValue appends the current value of label.
func appendValue(b []byte, state request.Request, rr *dnstest.Recorder, label string) []byte {
switch label {
case "{type}":
return append(b, state.Type()...)
case "{name}":
return append(b, state.Name()...)
case "{class}":
return append(b, state.Class()...)
case "{proto}":
return append(b, state.Proto()...)
case "{size}":
return strconv.AppendInt(b, int64(state.Req.Len()), 10)
case "{remote}":
return appendAddrToRFC3986(b, state.IP())
case "{port}":
return append(b, state.Port()...)
case "{local}":
return appendAddrToRFC3986(b, state.LocalIP())
// Header placeholders (case-insensitive).
case headerReplacer + "id}":
return strconv.AppendInt(b, int64(state.Req.Id), 10)
case headerReplacer + "opcode}":
return strconv.AppendInt(b, int64(state.Req.Opcode), 10)
case headerReplacer + "do}":
return strconv.AppendBool(b, state.Do())
case headerReplacer + "bufsize}":
return strconv.AppendInt(b, int64(state.Size()), 10)
// Recorded replacements.
case "{rcode}":
if rr == nil || rr.Msg == nil {
return append(b, EmptyValue...)
}
if rcode := dns.RcodeToString[rr.Rcode]; rcode != "" {
return append(b, rcode...)
}
return strconv.AppendInt(b, int64(rr.Rcode), 10)
case "{rsize}":
if rr == nil {
return append(b, EmptyValue...)
}
return strconv.AppendInt(b, int64(rr.Len), 10)
case "{duration}":
if rr == nil {
return append(b, EmptyValue...)
}
secs := time.Since(rr.Start).Seconds()
return append(strconv.AppendFloat(b, secs, 'f', -1, 64), 's')
case headerReplacer + "rflags}":
if rr != nil && rr.Msg != nil {
return appendFlags(b, rr.Msg.MsgHdr)
}
return append(b, EmptyValue...)
default:
return append(b, EmptyValue...)
}
}
// appendFlags checks all header flags and appends those
// that are set as a string separated with commas
func appendFlags(b []byte, h dns.MsgHdr) []byte {
origLen := len(b)
if h.Response {
b = append(b, "qr,"...)
}
if h.Authoritative {
b = append(b, "aa,"...)
}
if h.Truncated {
b = append(b, "tc,"...)
}
if h.RecursionDesired {
b = append(b, "rd,"...)
}
if h.RecursionAvailable {
b = append(b, "ra,"...)
}
if h.Zero {
b = append(b, "z,"...)
}
if h.AuthenticatedData {
b = append(b, "ad,"...)
}
if h.CheckingDisabled {
b = append(b, "cd,"...)
}
if n := len(b); n > origLen {
return b[:n-1] // trim trailing ','
}
return b
}
// appendAddrToRFC3986 will add brackets to the address if it is an IPv6 address.
func appendAddrToRFC3986(b []byte, addr string) []byte {
if strings.IndexByte(addr, ':') != -1 {
b = append(b, '[')
b = append(b, addr...)
b = append(b, ']')
} else {
b = append(b, addr...)
}
return b
}
type nodeType int
const (
typeLabel nodeType = iota // "{type}"
typeLiteral // "foo"
typeMetadata // "{/metadata}"
)
// A node represents a segment of a parsed format. For example: "A {type}"
// contains two nodes: "A " (literal); and "{type}" (label).
type node struct {
value string // Literal value, label or metadata label
typ nodeType
}
// A replacer is an ordered list of all the nodes in a format.
type replacer []node
func parseFormat(s string) replacer {
// Assume there is a literal between each label - its cheaper to over
// allocate once than allocate twice.
rep := make(replacer, 0, strings.Count(s, "{")*2)
for {
// We find the right bracket then backtrack to find the left bracket.
// This allows us to handle formats like: "{ {foo} }".
j := strings.IndexByte(s, '}')
if j < 0 {
break
}
i := strings.LastIndexByte(s[:j], '{')
if i < 0 {
// Handle: "A } {foo}" by treating "A }" as a literal
rep = append(rep, node{
value: s[:j+1],
typ: typeLiteral,
})
s = s[j+1:]
continue
}
val := s[i : j+1]
var typ nodeType
switch _, ok := labels[val]; {
case ok:
typ = typeLabel
case strings.HasPrefix(val, "{/"):
// Strip "{/}" from metadata labels
val = val[2 : len(val)-1]
typ = typeMetadata
default:
// Given: "A {X}" val is "{X}" expand it to the whole literal.
val = s[:j+1]
typ = typeLiteral
}
// Append any leading literal. Given "A {type}" the literal is "A "
if i != 0 && typ != typeLiteral {
rep = append(rep, node{
value: s[:i],
typ: typeLiteral,
})
}
rep = append(rep, node{
value: val,
typ: typ,
})
s = s[j+1:]
}
if len(s) != 0 {
rep = append(rep, node{
value: s,
typ: typeLiteral,
})
}
return rep
}
var replacerCache sync.Map // map[string]replacer
func loadFormat(s string) replacer {
if v, ok := replacerCache.Load(s); ok {
return v.(replacer)
}
v, _ := replacerCache.LoadOrStore(s, parseFormat(s))
return v.(replacer)
}
// bufPool stores pointers to scratch buffers.
var bufPool = sync.Pool{
New: func() interface{} {
return make([]byte, 0, 256)
},
}
func (r replacer) Replace(ctx context.Context, state request.Request, rr *dnstest.Recorder) string {
b := bufPool.Get().([]byte)
for _, s := range r {
switch s.typ {
case typeLabel:
b = appendValue(b, state, rr, s.value)
case typeLiteral:
b = append(b, s.value...)
case typeMetadata:
if fm := metadata.ValueFunc(ctx, s.value); fm != nil {
b = append(b, fm()...)
} else {
b = append(b, EmptyValue...)
}
}
}
s := string(b)
//nolint:staticcheck
bufPool.Put(b[:0])
return s
}