208 lines
4.4 KiB
Go
208 lines
4.4 KiB
Go
package replacer
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"strings"
|
|
"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 {
|
|
valueFunc func(request.Request, *dnstest.Recorder, string) string
|
|
labels []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 + "rflags}",
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
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 + "rflags}":
|
|
if rr != nil && rr.Msg != nil {
|
|
return flagsToString(rr.Msg.MsgHdr)
|
|
}
|
|
return EmptyValue
|
|
}
|
|
return EmptyValue
|
|
}
|
|
|
|
// 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(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)
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
|
|
b.WriteString(s[:idxStart])
|
|
b.WriteString(replacement)
|
|
s = s[endOffset+idxEnd+1:]
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
b.WriteString(s)
|
|
return b.String()
|
|
}
|
|
|
|
func boolToString(b bool) string {
|
|
if b {
|
|
return "true"
|
|
}
|
|
return "false"
|
|
}
|
|
|
|
// flagsToString checks all header flags and returns those
|
|
// that are set as a string separated with commas
|
|
func flagsToString(h dns.MsgHdr) string {
|
|
flags := make([]string, 7)
|
|
i := 0
|
|
|
|
if h.Response {
|
|
flags[i] = "qr"
|
|
i++
|
|
}
|
|
|
|
if h.Authoritative {
|
|
flags[i] = "aa"
|
|
i++
|
|
}
|
|
if h.Truncated {
|
|
flags[i] = "tc"
|
|
i++
|
|
}
|
|
if h.RecursionDesired {
|
|
flags[i] = "rd"
|
|
i++
|
|
}
|
|
if h.RecursionAvailable {
|
|
flags[i] = "ra"
|
|
i++
|
|
}
|
|
if h.Zero {
|
|
flags[i] = "z"
|
|
i++
|
|
}
|
|
if h.AuthenticatedData {
|
|
flags[i] = "ad"
|
|
i++
|
|
}
|
|
if h.CheckingDisabled {
|
|
flags[i] = "cd"
|
|
i++
|
|
}
|
|
return strings.Join(flags[:i], ",")
|
|
}
|
|
|
|
// addrToRFC3986 will add brackets to the address if it is an IPv6 address.
|
|
func addrToRFC3986(addr string) string {
|
|
if strings.Contains(addr, ":") {
|
|
return "[" + addr + "]"
|
|
}
|
|
return addr
|
|
}
|
|
|
|
const (
|
|
headerReplacer = "{>"
|
|
labelReplacer = "{/"
|
|
// EmptyValue is the default empty value.
|
|
EmptyValue = "-"
|
|
)
|