replacer: Implement file.* global replacements (#5463)

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
This commit is contained in:
Francis Lavoie 2024-04-24 16:26:18 -04:00 committed by GitHub
parent 6d97d8d87b
commit 797973944f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 195 additions and 65 deletions

View file

@ -0,0 +1 @@
foo

View file

@ -249,6 +249,12 @@ func (c *TemplateContext) executeTemplateInBuffer(tplName string, buf *bytes.Buf
func (c TemplateContext) funcPlaceholder(name string) string { func (c TemplateContext) funcPlaceholder(name string) string {
repl := c.Req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl := c.Req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
// For safety, we don't want to allow the file placeholder in
// templates because it could be used to read arbitrary files
// if the template contents were not trusted.
repl = repl.WithoutFile()
value, _ := repl.GetString(name) value, _ := repl.GetString(name)
return value return value
} }

View file

@ -16,6 +16,7 @@ package caddy
import ( import (
"fmt" "fmt"
"io"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -24,6 +25,8 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"go.uber.org/zap"
) )
// NewReplacer returns a new Replacer. // NewReplacer returns a new Replacer.
@ -32,9 +35,10 @@ func NewReplacer() *Replacer {
static: make(map[string]any), static: make(map[string]any),
mapMutex: &sync.RWMutex{}, mapMutex: &sync.RWMutex{},
} }
rep.providers = []ReplacerFunc{ rep.providers = []replacementProvider{
globalDefaultReplacements, globalDefaultReplacementProvider{},
rep.fromStatic, fileReplacementProvider{},
ReplacerFunc(rep.fromStatic),
} }
return rep return rep
} }
@ -46,8 +50,8 @@ func NewEmptyReplacer() *Replacer {
static: make(map[string]any), static: make(map[string]any),
mapMutex: &sync.RWMutex{}, mapMutex: &sync.RWMutex{},
} }
rep.providers = []ReplacerFunc{ rep.providers = []replacementProvider{
rep.fromStatic, ReplacerFunc(rep.fromStatic),
} }
return rep return rep
} }
@ -56,10 +60,25 @@ func NewEmptyReplacer() *Replacer {
// A default/empty Replacer is not valid; // A default/empty Replacer is not valid;
// use NewReplacer to make one. // use NewReplacer to make one.
type Replacer struct { type Replacer struct {
providers []ReplacerFunc providers []replacementProvider
static map[string]any
mapMutex *sync.RWMutex
}
static map[string]any // WithoutFile returns a copy of the current Replacer
mapMutex *sync.RWMutex // without support for the {file.*} placeholder, which
// may be unsafe in some contexts.
//
// EXPERIMENTAL: Subject to change or removal.
func (r *Replacer) WithoutFile() *Replacer {
rep := &Replacer{static: r.static}
for _, v := range r.providers {
if _, ok := v.(fileReplacementProvider); ok {
continue
}
rep.providers = append(rep.providers, v)
}
return rep
} }
// Map adds mapFunc to the list of value providers. // Map adds mapFunc to the list of value providers.
@ -79,7 +98,7 @@ func (r *Replacer) Set(variable string, value any) {
// the value and whether the variable was known. // the value and whether the variable was known.
func (r *Replacer) Get(variable string) (any, bool) { func (r *Replacer) Get(variable string) (any, bool) {
for _, mapFunc := range r.providers { for _, mapFunc := range r.providers {
if val, ok := mapFunc(variable); ok { if val, ok := mapFunc.replace(variable); ok {
return val, true return val, true
} }
} }
@ -298,14 +317,52 @@ func ToString(val any) string {
} }
} }
// ReplacerFunc is a function that returns a replacement // ReplacerFunc is a function that returns a replacement for the
// for the given key along with true if the function is able // given key along with true if the function is able to service
// to service that key (even if the value is blank). If the // that key (even if the value is blank). If the function does
// function does not recognize the key, false should be // not recognize the key, false should be returned.
// returned.
type ReplacerFunc func(key string) (any, bool) type ReplacerFunc func(key string) (any, bool)
func globalDefaultReplacements(key string) (any, bool) { func (f ReplacerFunc) replace(key string) (any, bool) {
return f(key)
}
// replacementProvider is a type that can provide replacements
// for placeholders. Allows for type assertion to determine
// which type of provider it is.
type replacementProvider interface {
replace(key string) (any, bool)
}
// fileReplacementsProvider handles {file.*} replacements,
// reading a file from disk and replacing with its contents.
type fileReplacementProvider struct{}
func (f fileReplacementProvider) replace(key string) (any, bool) {
if !strings.HasPrefix(key, filePrefix) {
return nil, false
}
filename := key[len(filePrefix):]
maxSize := 1024 * 1024
body, err := readFileIntoBuffer(filename, maxSize)
if err != nil {
wd, _ := os.Getwd()
Log().Error("placeholder: failed to read file",
zap.String("file", filename),
zap.String("working_dir", wd),
zap.Error(err))
return nil, true
}
return string(body), true
}
// globalDefaultReplacementsProvider handles replacements
// that can be used in any context, such as system variables,
// time, or environment variables.
type globalDefaultReplacementProvider struct{}
func (f globalDefaultReplacementProvider) replace(key string) (any, bool) {
// check environment variable // check environment variable
const envPrefix = "env." const envPrefix = "env."
if strings.HasPrefix(key, envPrefix) { if strings.HasPrefix(key, envPrefix) {
@ -347,6 +404,24 @@ func globalDefaultReplacements(key string) (any, bool) {
return nil, false return nil, false
} }
// readFileIntoBuffer reads the file at filePath into a size limited buffer.
func readFileIntoBuffer(filename string, size int) ([]byte, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
buffer := make([]byte, size)
n, err := file.Read(buffer)
if err != nil && err != io.EOF {
return nil, err
}
// slice the buffer to the actual size
return buffer[:n], nil
}
// ReplacementFunc is a function that is called when a // ReplacementFunc is a function that is called when a
// replacement is being performed. It receives the // replacement is being performed. It receives the
// variable (i.e. placeholder name) and the value that // variable (i.e. placeholder name) and the value that
@ -363,3 +438,5 @@ var nowFunc = time.Now
const ReplacerCtxKey CtxKey = "replacer" const ReplacerCtxKey CtxKey = "replacer"
const phOpen, phClose, phEscape = '{', '}', '\\' const phOpen, phClose, phEscape = '{', '}', '\\'
const filePrefix = "file."

View file

@ -240,9 +240,9 @@ func TestReplacerSet(t *testing.T) {
func TestReplacerReplaceKnown(t *testing.T) { func TestReplacerReplaceKnown(t *testing.T) {
rep := Replacer{ rep := Replacer{
mapMutex: &sync.RWMutex{}, mapMutex: &sync.RWMutex{},
providers: []ReplacerFunc{ providers: []replacementProvider{
// split our possible vars to two functions (to test if both functions are called) // split our possible vars to two functions (to test if both functions are called)
func(key string) (val any, ok bool) { ReplacerFunc(func(key string) (val any, ok bool) {
switch key { switch key {
case "test1": case "test1":
return "val1", true return "val1", true
@ -255,8 +255,8 @@ func TestReplacerReplaceKnown(t *testing.T) {
default: default:
return "NOOO", false return "NOOO", false
} }
}, }),
func(key string) (val any, ok bool) { ReplacerFunc(func(key string) (val any, ok bool) {
switch key { switch key {
case "1": case "1":
return "test-123", true return "test-123", true
@ -267,7 +267,7 @@ func TestReplacerReplaceKnown(t *testing.T) {
default: default:
return "NOOO", false return "NOOO", false
} }
}, }),
}, },
} }
@ -372,53 +372,99 @@ func TestReplacerMap(t *testing.T) {
} }
func TestReplacerNew(t *testing.T) { func TestReplacerNew(t *testing.T) {
rep := NewReplacer() repl := NewReplacer()
if len(rep.providers) != 2 { if len(repl.providers) != 3 {
t.Errorf("Expected providers length '%v' got length '%v'", 2, len(rep.providers)) t.Errorf("Expected providers length '%v' got length '%v'", 3, len(repl.providers))
} else { }
// test if default global replacements are added as the first provider
hostname, _ := os.Hostname()
wd, _ := os.Getwd()
os.Setenv("CADDY_REPLACER_TEST", "envtest")
defer os.Setenv("CADDY_REPLACER_TEST", "")
for _, tc := range []struct { // test if default global replacements are added as the first provider
variable string hostname, _ := os.Hostname()
value string wd, _ := os.Getwd()
}{ os.Setenv("CADDY_REPLACER_TEST", "envtest")
{ defer os.Setenv("CADDY_REPLACER_TEST", "")
variable: "system.hostname",
value: hostname, for _, tc := range []struct {
}, variable string
{ value string
variable: "system.slash", }{
value: string(filepath.Separator), {
}, variable: "system.hostname",
{ value: hostname,
variable: "system.os", },
value: runtime.GOOS, {
}, variable: "system.slash",
{ value: string(filepath.Separator),
variable: "system.arch", },
value: runtime.GOARCH, {
}, variable: "system.os",
{ value: runtime.GOOS,
variable: "system.wd", },
value: wd, {
}, variable: "system.arch",
{ value: runtime.GOARCH,
variable: "env.CADDY_REPLACER_TEST", },
value: "envtest", {
}, variable: "system.wd",
} { value: wd,
if val, ok := rep.providers[0](tc.variable); ok { },
if val != tc.value { {
t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val) variable: "env.CADDY_REPLACER_TEST",
} value: "envtest",
} else { },
t.Errorf("Expected key '%s' to be recognized by first provider", tc.variable) } {
if val, ok := repl.providers[0].replace(tc.variable); ok {
if val != tc.value {
t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
} }
} else {
t.Errorf("Expected key '%s' to be recognized by first provider", tc.variable)
}
}
// test if file provider is added as the second provider
for _, tc := range []struct {
variable string
value string
}{
{
variable: "file.caddytest/integration/testdata/foo.txt",
value: "foo",
},
} {
if val, ok := repl.providers[1].replace(tc.variable); ok {
if val != tc.value {
t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
}
} else {
t.Errorf("Expected key '%s' to be recognized by second provider", tc.variable)
}
}
}
func TestReplacerNewWithoutFile(t *testing.T) {
repl := NewReplacer().WithoutFile()
for _, tc := range []struct {
variable string
value string
notFound bool
}{
{
variable: "file.caddytest/integration/testdata/foo.txt",
notFound: true,
},
{
variable: "system.os",
value: runtime.GOOS,
},
} {
if val, ok := repl.Get(tc.variable); ok && !tc.notFound {
if val != tc.value {
t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
}
} else if !tc.notFound {
t.Errorf("Expected key '%s' to be recognized", tc.variable)
} }
} }
} }
@ -464,7 +510,7 @@ func BenchmarkReplacer(b *testing.B) {
func testReplacer() Replacer { func testReplacer() Replacer {
return Replacer{ return Replacer{
providers: make([]ReplacerFunc, 0), providers: make([]replacementProvider, 0),
static: make(map[string]any), static: make(map[string]any),
mapMutex: &sync.RWMutex{}, mapMutex: &sync.RWMutex{},
} }