277 lines
6.7 KiB
Go
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
|
|
}
|