aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorFrancis Lavoie <[email protected]>2024-04-24 16:26:18 -0400
committerGitHub <[email protected]>2024-04-24 16:26:18 -0400
commit797973944f9bf60c84350a38848613b6247a66eb (patch)
treef50da352bdfd3e64d1ab6a86e5625a350f28d604
parent6d97d8d87beb788d19a4084d07ec9157e5705b13 (diff)
downloadcaddy-797973944f9bf60c84350a38848613b6247a66eb.tar.gz
caddy-797973944f9bf60c84350a38848613b6247a66eb.zip
replacer: Implement `file.*` global replacements (#5463)
Co-authored-by: Matt Holt <[email protected]> Co-authored-by: Mohammed Al Sahaf <[email protected]>
-rw-r--r--caddytest/integration/testdata/foo.txt1
-rw-r--r--modules/caddyhttp/templates/tplcontext.go6
-rw-r--r--replacer.go107
-rw-r--r--replacer_test.go150
4 files changed, 197 insertions, 67 deletions
diff --git a/caddytest/integration/testdata/foo.txt b/caddytest/integration/testdata/foo.txt
new file mode 100644
index 000000000..191028156
--- /dev/null
+++ b/caddytest/integration/testdata/foo.txt
@@ -0,0 +1 @@
+foo \ No newline at end of file
diff --git a/modules/caddyhttp/templates/tplcontext.go b/modules/caddyhttp/templates/tplcontext.go
index 8ba64200f..4c7c86e13 100644
--- a/modules/caddyhttp/templates/tplcontext.go
+++ b/modules/caddyhttp/templates/tplcontext.go
@@ -249,6 +249,12 @@ func (c *TemplateContext) executeTemplateInBuffer(tplName string, buf *bytes.Buf
func (c TemplateContext) funcPlaceholder(name string) string {
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)
return value
}
diff --git a/replacer.go b/replacer.go
index 2ad5b8bcb..e5d2913e9 100644
--- a/replacer.go
+++ b/replacer.go
@@ -16,6 +16,7 @@ package caddy
import (
"fmt"
+ "io"
"net/http"
"os"
"path/filepath"
@@ -24,6 +25,8 @@ import (
"strings"
"sync"
"time"
+
+ "go.uber.org/zap"
)
// NewReplacer returns a new Replacer.
@@ -32,9 +35,10 @@ func NewReplacer() *Replacer {
static: make(map[string]any),
mapMutex: &sync.RWMutex{},
}
- rep.providers = []ReplacerFunc{
- globalDefaultReplacements,
- rep.fromStatic,
+ rep.providers = []replacementProvider{
+ globalDefaultReplacementProvider{},
+ fileReplacementProvider{},
+ ReplacerFunc(rep.fromStatic),
}
return rep
}
@@ -46,8 +50,8 @@ func NewEmptyReplacer() *Replacer {
static: make(map[string]any),
mapMutex: &sync.RWMutex{},
}
- rep.providers = []ReplacerFunc{
- rep.fromStatic,
+ rep.providers = []replacementProvider{
+ ReplacerFunc(rep.fromStatic),
}
return rep
}
@@ -56,10 +60,25 @@ func NewEmptyReplacer() *Replacer {
// A default/empty Replacer is not valid;
// use NewReplacer to make one.
type Replacer struct {
- providers []ReplacerFunc
+ providers []replacementProvider
+ static map[string]any
+ mapMutex *sync.RWMutex
+}
- static map[string]any
- 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.
@@ -79,7 +98,7 @@ func (r *Replacer) Set(variable string, value any) {
// the value and whether the variable was known.
func (r *Replacer) Get(variable string) (any, bool) {
for _, mapFunc := range r.providers {
- if val, ok := mapFunc(variable); ok {
+ if val, ok := mapFunc.replace(variable); ok {
return val, true
}
}
@@ -298,14 +317,52 @@ func ToString(val any) string {
}
}
-// ReplacerFunc is a function that returns a replacement
-// for the given key along with true if the function is able
-// to service that key (even if the value is blank). If the
-// function does not recognize the key, false should be
-// returned.
+// ReplacerFunc is a function that returns a replacement for the
+// given key along with true if the function is able to service
+// that key (even if the value is blank). If the function does
+// not recognize the key, false should be returned.
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
const envPrefix = "env."
if strings.HasPrefix(key, envPrefix) {
@@ -347,6 +404,24 @@ func globalDefaultReplacements(key string) (any, bool) {
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
// replacement is being performed. It receives the
// variable (i.e. placeholder name) and the value that
@@ -363,3 +438,5 @@ var nowFunc = time.Now
const ReplacerCtxKey CtxKey = "replacer"
const phOpen, phClose, phEscape = '{', '}', '\\'
+
+const filePrefix = "file."
diff --git a/replacer_test.go b/replacer_test.go
index d18ec8eea..cf4d321b6 100644
--- a/replacer_test.go
+++ b/replacer_test.go
@@ -240,9 +240,9 @@ func TestReplacerSet(t *testing.T) {
func TestReplacerReplaceKnown(t *testing.T) {
rep := Replacer{
mapMutex: &sync.RWMutex{},
- providers: []ReplacerFunc{
+ providers: []replacementProvider{
// 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 {
case "test1":
return "val1", true
@@ -255,8 +255,8 @@ func TestReplacerReplaceKnown(t *testing.T) {
default:
return "NOOO", false
}
- },
- func(key string) (val any, ok bool) {
+ }),
+ ReplacerFunc(func(key string) (val any, ok bool) {
switch key {
case "1":
return "test-123", true
@@ -267,7 +267,7 @@ func TestReplacerReplaceKnown(t *testing.T) {
default:
return "NOOO", false
}
- },
+ }),
},
}
@@ -372,53 +372,99 @@ func TestReplacerMap(t *testing.T) {
}
func TestReplacerNew(t *testing.T) {
- rep := NewReplacer()
-
- 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
- hostname, _ := os.Hostname()
- wd, _ := os.Getwd()
- os.Setenv("CADDY_REPLACER_TEST", "envtest")
- defer os.Setenv("CADDY_REPLACER_TEST", "")
-
- for _, tc := range []struct {
- variable string
- value string
- }{
- {
- variable: "system.hostname",
- value: hostname,
- },
- {
- variable: "system.slash",
- value: string(filepath.Separator),
- },
- {
- variable: "system.os",
- value: runtime.GOOS,
- },
- {
- variable: "system.arch",
- value: runtime.GOARCH,
- },
- {
- variable: "system.wd",
- value: wd,
- },
- {
- variable: "env.CADDY_REPLACER_TEST",
- value: "envtest",
- },
- } {
- 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)
- }
- } else {
- t.Errorf("Expected key '%s' to be recognized by first provider", tc.variable)
+ repl := NewReplacer()
+
+ if len(repl.providers) != 3 {
+ t.Errorf("Expected providers length '%v' got length '%v'", 3, len(repl.providers))
+ }
+
+ // 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 {
+ variable string
+ value string
+ }{
+ {
+ variable: "system.hostname",
+ value: hostname,
+ },
+ {
+ variable: "system.slash",
+ value: string(filepath.Separator),
+ },
+ {
+ variable: "system.os",
+ value: runtime.GOOS,
+ },
+ {
+ variable: "system.arch",
+ value: runtime.GOARCH,
+ },
+ {
+ variable: "system.wd",
+ value: wd,
+ },
+ {
+ variable: "env.CADDY_REPLACER_TEST",
+ value: "envtest",
+ },
+ } {
+ 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 {
return Replacer{
- providers: make([]ReplacerFunc, 0),
+ providers: make([]replacementProvider, 0),
static: make(map[string]any),
mapMutex: &sync.RWMutex{},
}