@marcel91200 / caddy Mirror · read-only / commits / 79797394
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 12, 13:15 UTC

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>

Francis Lavoie committed Apr 24, 2024 at 16:26 UTC 797973944f9bf60c84350a38848613b6247a66eb
4 files changed +197 -67
caddytest/integration/testdata/foo.txt new
+1
@@ -0,0 +1 @@
1 +foo
\ No newline at end of file
modules/caddyhttp/templates/tplcontext.go
+6
@@ -249,6 +249,12 @@ func (c *TemplateContext) executeTemplateInBuffer(tplName string, buf *bytes.Buf
249
250 func (c TemplateContext) funcPlaceholder(name string) string {
251 repl := c.Req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
252 +
253 + // For safety, we don't want to allow the file placeholder in
254 + // templates because it could be used to read arbitrary files
255 + // if the template contents were not trusted.
256 + repl = repl.WithoutFile()
257 +
258 value, _ := repl.GetString(name)
259 return value
260 }
replacer.go
+92 -15
@@ -16,6 +16,7 @@ package caddy
16
17 import (
18 "fmt"
19 + "io"
20 "net/http"
21 "os"
22 "path/filepath"
@@ -24,6 +25,8 @@ import (
25 "strings"
26 "sync"
27 "time"
28 +
29 + "go.uber.org/zap"
30 )
31
32 // NewReplacer returns a new Replacer.
@@ -32,9 +35,10 @@ func NewReplacer() *Replacer {
35 static: make(map[string]any),
36 mapMutex: &sync.RWMutex{},
37 }
35 - rep.providers = []ReplacerFunc{
36 - globalDefaultReplacements,
37 - rep.fromStatic,
38 + rep.providers = []replacementProvider{
39 + globalDefaultReplacementProvider{},
40 + fileReplacementProvider{},
41 + ReplacerFunc(rep.fromStatic),
42 }
43 return rep
44 }
@@ -46,8 +50,8 @@ func NewEmptyReplacer() *Replacer {
50 static: make(map[string]any),
51 mapMutex: &sync.RWMutex{},
52 }
49 - rep.providers = []ReplacerFunc{
50 - rep.fromStatic,
53 + rep.providers = []replacementProvider{
54 + ReplacerFunc(rep.fromStatic),
55 }
56 return rep
57 }
@@ -56,10 +60,25 @@ func NewEmptyReplacer() *Replacer {
60 // A default/empty Replacer is not valid;
61 // use NewReplacer to make one.
62 type Replacer struct {
59 - providers []ReplacerFunc
63 + providers []replacementProvider
64 + static map[string]any
65 + mapMutex *sync.RWMutex
66 +}
67
61 - static map[string]any
62 - mapMutex *sync.RWMutex
68 +// WithoutFile returns a copy of the current Replacer
69 +// without support for the {file.*} placeholder, which
70 +// may be unsafe in some contexts.
71 +//
72 +// EXPERIMENTAL: Subject to change or removal.
73 +func (r *Replacer) WithoutFile() *Replacer {
74 + rep := &Replacer{static: r.static}
75 + for _, v := range r.providers {
76 + if _, ok := v.(fileReplacementProvider); ok {
77 + continue
78 + }
79 + rep.providers = append(rep.providers, v)
80 + }
81 + return rep
82 }
83
84 // Map adds mapFunc to the list of value providers.
@@ -79,7 +98,7 @@ func (r *Replacer) Set(variable string, value any) {
98 // the value and whether the variable was known.
99 func (r *Replacer) Get(variable string) (any, bool) {
100 for _, mapFunc := range r.providers {
82 - if val, ok := mapFunc(variable); ok {
101 + if val, ok := mapFunc.replace(variable); ok {
102 return val, true
103 }
104 }
@@ -298,14 +317,52 @@ func ToString(val any) string {
317 }
318 }
319
301 -// ReplacerFunc is a function that returns a replacement
302 -// for the given key along with true if the function is able
303 -// to service that key (even if the value is blank). If the
304 -// function does not recognize the key, false should be
305 -// returned.
320 +// ReplacerFunc is a function that returns a replacement for the
321 +// given key along with true if the function is able to service
322 +// that key (even if the value is blank). If the function does
323 +// not recognize the key, false should be returned.
324 type ReplacerFunc func(key string) (any, bool)
325
308 -func globalDefaultReplacements(key string) (any, bool) {
326 +func (f ReplacerFunc) replace(key string) (any, bool) {
327 + return f(key)
328 +}
329 +
330 +// replacementProvider is a type that can provide replacements
331 +// for placeholders. Allows for type assertion to determine
332 +// which type of provider it is.
333 +type replacementProvider interface {
334 + replace(key string) (any, bool)
335 +}
336 +
337 +// fileReplacementsProvider handles {file.*} replacements,
338 +// reading a file from disk and replacing with its contents.
339 +type fileReplacementProvider struct{}
340 +
341 +func (f fileReplacementProvider) replace(key string) (any, bool) {
342 + if !strings.HasPrefix(key, filePrefix) {
343 + return nil, false
344 + }
345 +
346 + filename := key[len(filePrefix):]
347 + maxSize := 1024 * 1024
348 + body, err := readFileIntoBuffer(filename, maxSize)
349 + if err != nil {
350 + wd, _ := os.Getwd()
351 + Log().Error("placeholder: failed to read file",
352 + zap.String("file", filename),
353 + zap.String("working_dir", wd),
354 + zap.Error(err))
355 + return nil, true
356 + }
357 + return string(body), true
358 +}
359 +
360 +// globalDefaultReplacementsProvider handles replacements
361 +// that can be used in any context, such as system variables,
362 +// time, or environment variables.
363 +type globalDefaultReplacementProvider struct{}
364 +
365 +func (f globalDefaultReplacementProvider) replace(key string) (any, bool) {
366 // check environment variable
367 const envPrefix = "env."
368 if strings.HasPrefix(key, envPrefix) {
@@ -347,6 +404,24 @@ func globalDefaultReplacements(key string) (any, bool) {
404 return nil, false
405 }
406
407 +// readFileIntoBuffer reads the file at filePath into a size limited buffer.
408 +func readFileIntoBuffer(filename string, size int) ([]byte, error) {
409 + file, err := os.Open(filename)
410 + if err != nil {
411 + return nil, err
412 + }
413 + defer file.Close()
414 +
415 + buffer := make([]byte, size)
416 + n, err := file.Read(buffer)
417 + if err != nil && err != io.EOF {
418 + return nil, err
419 + }
420 +
421 + // slice the buffer to the actual size
422 + return buffer[:n], nil
423 +}
424 +
425 // ReplacementFunc is a function that is called when a
426 // replacement is being performed. It receives the
427 // variable (i.e. placeholder name) and the value that
@@ -363,3 +438,5 @@ var nowFunc = time.Now
438 const ReplacerCtxKey CtxKey = "replacer"
439
440 const phOpen, phClose, phEscape = '{', '}', '\\'
441 +
442 +const filePrefix = "file."
replacer_test.go
+98 -52
@@ -240,9 +240,9 @@ func TestReplacerSet(t *testing.T) {
240 func TestReplacerReplaceKnown(t *testing.T) {
241 rep := Replacer{
242 mapMutex: &sync.RWMutex{},
243 - providers: []ReplacerFunc{
243 + providers: []replacementProvider{
244 // split our possible vars to two functions (to test if both functions are called)
245 - func(key string) (val any, ok bool) {
245 + ReplacerFunc(func(key string) (val any, ok bool) {
246 switch key {
247 case "test1":
248 return "val1", true
@@ -255,8 +255,8 @@ func TestReplacerReplaceKnown(t *testing.T) {
255 default:
256 return "NOOO", false
257 }
258 - },
259 - func(key string) (val any, ok bool) {
258 + }),
259 + ReplacerFunc(func(key string) (val any, ok bool) {
260 switch key {
261 case "1":
262 return "test-123", true
@@ -267,7 +267,7 @@ func TestReplacerReplaceKnown(t *testing.T) {
267 default:
268 return "NOOO", false
269 }
270 - },
270 + }),
271 },
272 }
273
@@ -372,53 +372,99 @@ func TestReplacerMap(t *testing.T) {
372 }
373
374 func TestReplacerNew(t *testing.T) {
375 - rep := NewReplacer()
376 -
377 - if len(rep.providers) != 2 {
378 - t.Errorf("Expected providers length '%v' got length '%v'", 2, len(rep.providers))
379 - } else {
380 - // test if default global replacements are added as the first provider
381 - hostname, _ := os.Hostname()
382 - wd, _ := os.Getwd()
383 - os.Setenv("CADDY_REPLACER_TEST", "envtest")
384 - defer os.Setenv("CADDY_REPLACER_TEST", "")
385 -
386 - for _, tc := range []struct {
387 - variable string
388 - value string
389 - }{
390 - {
391 - variable: "system.hostname",
392 - value: hostname,
393 - },
394 - {
395 - variable: "system.slash",
396 - value: string(filepath.Separator),
397 - },
398 - {
399 - variable: "system.os",
400 - value: runtime.GOOS,
401 - },
402 - {
403 - variable: "system.arch",
404 - value: runtime.GOARCH,
405 - },
406 - {
407 - variable: "system.wd",
408 - value: wd,
409 - },
410 - {
411 - variable: "env.CADDY_REPLACER_TEST",
412 - value: "envtest",
413 - },
414 - } {
415 - if val, ok := rep.providers[0](tc.variable); ok {
416 - if val != tc.value {
417 - t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
418 - }
419 - } else {
420 - t.Errorf("Expected key '%s' to be recognized by first provider", tc.variable)
375 + repl := NewReplacer()
376 +
377 + if len(repl.providers) != 3 {
378 + t.Errorf("Expected providers length '%v' got length '%v'", 3, len(repl.providers))
379 + }
380 +
381 + // test if default global replacements are added as the first provider
382 + hostname, _ := os.Hostname()
383 + wd, _ := os.Getwd()
384 + os.Setenv("CADDY_REPLACER_TEST", "envtest")
385 + defer os.Setenv("CADDY_REPLACER_TEST", "")
386 +
387 + for _, tc := range []struct {
388 + variable string
389 + value string
390 + }{
391 + {
392 + variable: "system.hostname",
393 + value: hostname,
394 + },
395 + {
396 + variable: "system.slash",
397 + value: string(filepath.Separator),
398 + },
399 + {
400 + variable: "system.os",
401 + value: runtime.GOOS,
402 + },
403 + {
404 + variable: "system.arch",
405 + value: runtime.GOARCH,
406 + },
407 + {
408 + variable: "system.wd",
409 + value: wd,
410 + },
411 + {
412 + variable: "env.CADDY_REPLACER_TEST",
413 + value: "envtest",
414 + },
415 + } {
416 + if val, ok := repl.providers[0].replace(tc.variable); ok {
417 + if val != tc.value {
418 + t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
419 + }
420 + } else {
421 + t.Errorf("Expected key '%s' to be recognized by first provider", tc.variable)
422 + }
423 + }
424 +
425 + // test if file provider is added as the second provider
426 + for _, tc := range []struct {
427 + variable string
428 + value string
429 + }{
430 + {
431 + variable: "file.caddytest/integration/testdata/foo.txt",
432 + value: "foo",
433 + },
434 + } {
435 + if val, ok := repl.providers[1].replace(tc.variable); ok {
436 + if val != tc.value {
437 + t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
438 + }
439 + } else {
440 + t.Errorf("Expected key '%s' to be recognized by second provider", tc.variable)
441 + }
442 + }
443 +}
444 +
445 +func TestReplacerNewWithoutFile(t *testing.T) {
446 + repl := NewReplacer().WithoutFile()
447 +
448 + for _, tc := range []struct {
449 + variable string
450 + value string
451 + notFound bool
452 + }{
453 + {
454 + variable: "file.caddytest/integration/testdata/foo.txt",
455 + notFound: true,
456 + },
457 + {
458 + variable: "system.os",
459 + value: runtime.GOOS,
460 + },
461 + } {
462 + if val, ok := repl.Get(tc.variable); ok && !tc.notFound {
463 + if val != tc.value {
464 + t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
465 }
466 + } else if !tc.notFound {
467 + t.Errorf("Expected key '%s' to be recognized", tc.variable)
468 }
469 }
470 }
@@ -464,7 +510,7 @@ func BenchmarkReplacer(b *testing.B) {
510
511 func testReplacer() Replacer {
512 return Replacer{
467 - providers: make([]ReplacerFunc, 0),
513 + providers: make([]replacementProvider, 0),
514 static: make(map[string]any),
515 mapMutex: &sync.RWMutex{},
516 }