mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-27 06:03:48 +03:00
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:
parent
6d97d8d87b
commit
797973944f
4 changed files with 195 additions and 65 deletions
1
caddytest/integration/testdata/foo.txt
vendored
Normal file
1
caddytest/integration/testdata/foo.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
foo
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
105
replacer.go
105
replacer.go
|
@ -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,12 +60,27 @@ 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
|
static map[string]any
|
||||||
mapMutex *sync.RWMutex
|
mapMutex *sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithoutFile returns a copy of the current Replacer
|
||||||
|
// 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.
|
||||||
// mapFunc will be executed only at replace-time.
|
// mapFunc will be executed only at replace-time.
|
||||||
func (r *Replacer) Map(mapFunc ReplacerFunc) {
|
func (r *Replacer) Map(mapFunc ReplacerFunc) {
|
||||||
|
@ -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."
|
||||||
|
|
|
@ -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,11 +372,12 @@ func TestReplacerMap(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReplacerNew(t *testing.T) {
|
func TestReplacerNew(t *testing.T) {
|
||||||
rep := NewReplacer()
|
repl := NewReplacer()
|
||||||
|
|
||||||
|
if len(repl.providers) != 3 {
|
||||||
|
t.Errorf("Expected providers length '%v' got length '%v'", 3, len(repl.providers))
|
||||||
|
}
|
||||||
|
|
||||||
if len(rep.providers) != 2 {
|
|
||||||
t.Errorf("Expected providers length '%v' got length '%v'", 2, len(rep.providers))
|
|
||||||
} else {
|
|
||||||
// test if default global replacements are added as the first provider
|
// test if default global replacements are added as the first provider
|
||||||
hostname, _ := os.Hostname()
|
hostname, _ := os.Hostname()
|
||||||
wd, _ := os.Getwd()
|
wd, _ := os.Getwd()
|
||||||
|
@ -412,7 +413,7 @@ func TestReplacerNew(t *testing.T) {
|
||||||
value: "envtest",
|
value: "envtest",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
if val, ok := rep.providers[0](tc.variable); ok {
|
if val, ok := repl.providers[0].replace(tc.variable); ok {
|
||||||
if val != tc.value {
|
if val != tc.value {
|
||||||
t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
|
t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
|
||||||
}
|
}
|
||||||
|
@ -420,6 +421,51 @@ func TestReplacerNew(t *testing.T) {
|
||||||
t.Errorf("Expected key '%s' to be recognized by first provider", tc.variable)
|
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{},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue