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
}