Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
caddyfile: Preprocess env vars in {$THIS} format (#2963)
* transform a caddyfile with environment variables * support adapt time and runtime variables in the caddyfile * caddyfile: Pre-process environment variables before parsing Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Mark Sargent committed
Jan 10, 2020 at 05:40 UTC
7c419d5349837b50c9d87be88fc438f8c4e475b9
3 files changed
+124
-120
caddyconfig/caddyfile/adapter.go
+1
-2
@@ -15,7 +15,6 @@
15
package caddyfile
16
17
import (
18
- "bytes"
18
"encoding/json"
19
"fmt"
20
@@ -42,7 +41,7 @@ func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []c
41
filename = "Caddyfile"
42
}
43
45
- serverBlocks, err := Parse(filename, bytes.NewReader(body))
44
+ serverBlocks, err := Parse(filename, body)
45
if err != nil {
46
return nil, nil, err
47
}
caddyconfig/caddyfile/parse.go
+49
-37
@@ -15,6 +15,7 @@
15
package caddyfile
16
17
import (
18
+ "bytes"
19
"io"
20
"log"
21
"os"
@@ -28,8 +29,12 @@ import (
29
// Directives that do not appear in validDirectives will cause
30
// an error. If you do not want to check for valid directives,
31
// pass in nil instead.
31
-func Parse(filename string, input io.Reader) ([]ServerBlock, error) {
32
- tokens, err := allTokens(filename, input)
32
+//
33
+// Environment variables in {$ENVIRONMENT_VARIABLE} notation
34
+// will be replaced before parsing begins.
35
+func Parse(filename string, input []byte) ([]ServerBlock, error) {
36
+ input = replaceEnvVars(input)
37
+ tokens, err := allTokens(filename, bytes.NewReader(input))
38
if err != nil {
39
return nil, err
40
}
@@ -37,6 +42,41 @@ func Parse(filename string, input io.Reader) ([]ServerBlock, error) {
42
return p.parseAll()
43
}
44
45
+// replaceEnvVars replaces all occurrences of environment variables.
46
+func replaceEnvVars(input []byte) []byte {
47
+ var offset int
48
+ for {
49
+ begin := bytes.Index(input[offset:], spanOpen)
50
+ if begin < 0 {
51
+ break
52
+ }
53
+ begin += offset // make beginning relative to input, not offset
54
+ end := bytes.Index(input[begin+len(spanOpen):], spanClose)
55
+ if end < 0 {
56
+ break
57
+ }
58
+ end += begin + len(spanOpen) // make end relative to input, not begin
59
+
60
+ // get the name; if there is no name, skip it
61
+ envVarName := input[begin+len(spanOpen) : end]
62
+ if len(envVarName) == 0 {
63
+ offset = end + len(spanClose)
64
+ continue
65
+ }
66
+
67
+ // get the value of the environment variable
68
+ envVarValue := []byte(os.Getenv(string(envVarName)))
69
+
70
+ // splice in the value
71
+ input = append(input[:begin],
72
+ append(envVarValue, input[end+len(spanClose):]...)...)
73
+
74
+ // continue at the end of the replacement
75
+ offset = begin + len(envVarValue)
76
+ }
77
+ return input
78
+}
79
+
80
// allTokens lexes the entire input, but does not parse it.
81
// It returns all the tokens from the input, unstructured
82
// and in order.
@@ -128,7 +168,7 @@ func (p *parser) addresses() error {
168
var expectingAnother bool
169
170
for {
131
- tkn := replaceEnvVars(p.Val())
171
+ tkn := p.Val()
172
173
// special case: import directive replaces tokens during parse-time
174
if tkn == "import" && p.isNewLine() {
@@ -245,7 +285,7 @@ func (p *parser) doImport() error {
285
if !p.NextArg() {
286
return p.ArgErr()
287
}
248
- importPattern := replaceEnvVars(p.Val())
288
+ importPattern := p.Val()
289
if importPattern == "" {
290
return p.Err("Import requires a non-empty filepath")
291
}
@@ -353,8 +393,6 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
393
// are loaded into the current server block for later use
394
// by directive setup functions.
395
func (p *parser) directive() error {
356
- // evaluate any env vars in directive token
357
- p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
396
397
// a segment is a list of tokens associated with this directive
398
var segment Segment
@@ -379,7 +417,7 @@ func (p *parser) directive() error {
417
p.cursor-- // cursor is advanced when we continue, so roll back one more
418
continue
419
}
382
- p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
420
+
421
segment = append(segment, p.Token())
422
}
423
@@ -414,36 +452,6 @@ func (p *parser) closeCurlyBrace() error {
452
return nil
453
}
454
417
-// replaceEnvVars replaces environment variables that appear in the token
418
-// and understands both the $UNIX and %WINDOWS% syntaxes.
419
-func replaceEnvVars(s string) string {
420
- s = replaceEnvReferences(s, "{%", "%}")
421
- s = replaceEnvReferences(s, "{$", "}")
422
- return s
423
-}
424
-
425
-// replaceEnvReferences performs the actual replacement of env variables
426
-// in s, given the placeholder start and placeholder end strings.
427
-func replaceEnvReferences(s, refStart, refEnd string) string {
428
- index := strings.Index(s, refStart)
429
- for index != -1 {
430
- endIndex := strings.Index(s[index:], refEnd)
431
- if endIndex == -1 {
432
- break
433
- }
434
-
435
- endIndex += index
436
- if endIndex > index+len(refStart) {
437
- ref := s[index : endIndex+len(refEnd)]
438
- s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1)
439
- } else {
440
- return s
441
- }
442
- index = strings.Index(s, refStart)
443
- }
444
- return s
445
-}
446
-
455
func (p *parser) isSnippet() (bool, string) {
456
keys := p.block.Keys
457
// A snippet block is a single key with parens. Nothing else qualifies.
@@ -514,3 +522,7 @@ func (s Segment) Directive() string {
522
}
523
return ""
524
}
525
+
526
+// spanOpen and spanClose are used to bound spans that
527
+// contain the name of an environment variable.
528
+var spanOpen, spanClose = []byte{'{', '$'}, []byte{'}'}
caddyconfig/caddyfile/parse_test.go
+74
-81
@@ -15,6 +15,7 @@
15
package caddyfile
16
17
import (
18
+ "bytes"
19
"io/ioutil"
20
"os"
21
"path/filepath"
@@ -473,89 +474,81 @@ func TestParseAll(t *testing.T) {
474
}
475
476
func TestEnvironmentReplacement(t *testing.T) {
476
- os.Setenv("PORT", "8080")
477
- os.Setenv("ADDRESS", "servername.com")
477
os.Setenv("FOOBAR", "foobar")
479
- os.Setenv("PARTIAL_DIR", "r1")
478
481
- // basic test; unix-style env vars
482
- p := testParser(`{$ADDRESS}`)
483
- blocks, _ := p.parseAll()
484
- if actual, expected := blocks[0].Keys[0], "servername.com"; expected != actual {
485
- t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
486
- }
487
-
488
- // basic test; unix-style env vars
489
- p = testParser(`di{$PARTIAL_DIR}`)
490
- blocks, _ = p.parseAll()
491
- if actual, expected := blocks[0].Keys[0], "dir1"; expected != actual {
492
- t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
493
- }
494
-
495
- // multiple vars per token
496
- p = testParser(`{$ADDRESS}:{$PORT}`)
497
- blocks, _ = p.parseAll()
498
- if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
499
- t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
500
- }
501
-
502
- // windows-style var and unix style in same token
503
- p = testParser(`{%ADDRESS%}:{$PORT}`)
504
- blocks, _ = p.parseAll()
505
- if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
506
- t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
507
- }
508
-
509
- // reverse order
510
- p = testParser(`{$ADDRESS}:{%PORT%}`)
511
- blocks, _ = p.parseAll()
512
- if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
513
- t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
514
- }
515
-
516
- // env var in server block body as argument
517
- p = testParser(":{%PORT%}\ndir1 {$FOOBAR}")
518
- blocks, _ = p.parseAll()
519
- if actual, expected := blocks[0].Keys[0], ":8080"; expected != actual {
520
- t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
521
- }
522
- if actual, expected := blocks[0].Segments[0][1].Text, "foobar"; expected != actual {
523
- t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
524
- }
525
-
526
- // combined windows env vars in argument
527
- p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}")
528
- blocks, _ = p.parseAll()
529
- if actual, expected := blocks[0].Segments[0][1].Text, "servername.com/foobar"; expected != actual {
530
- t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
531
- }
532
-
533
- // malformed env var (windows)
534
- p = testParser(":1234\ndir1 {%ADDRESS}")
535
- blocks, _ = p.parseAll()
536
- if actual, expected := blocks[0].Segments[0][1].Text, "{%ADDRESS}"; expected != actual {
537
- t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
538
- }
539
-
540
- // malformed (non-existent) env var (unix)
541
- p = testParser(`:{$PORT$}`)
542
- blocks, _ = p.parseAll()
543
- if actual, expected := blocks[0].Keys[0], ":"; expected != actual {
544
- t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
545
- }
546
-
547
- // in quoted field
548
- p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"")
549
- blocks, _ = p.parseAll()
550
- if actual, expected := blocks[0].Segments[0][1].Text, "Test foobar test"; expected != actual {
551
- t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
552
- }
553
-
554
- // after end token
555
- p = testParser(":1234\nanswer \"{{ .Name }} {$FOOBAR}\"")
556
- blocks, _ = p.parseAll()
557
- if actual, expected := blocks[0].Segments[0][1].Text, "{{ .Name }} foobar"; expected != actual {
558
- t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
479
+ for i, test := range []struct {
480
+ input string
481
+ expect string
482
+ }{
483
+ {
484
+ input: "",
485
+ expect: "",
486
+ },
487
+ {
488
+ input: "foo",
489
+ expect: "foo",
490
+ },
491
+ {
492
+ input: "{$NOT_SET}",
493
+ expect: "",
494
+ },
495
+ {
496
+ input: "foo{$NOT_SET}bar",
497
+ expect: "foobar",
498
+ },
499
+ {
500
+ input: "{$FOOBAR}",
501
+ expect: "foobar",
502
+ },
503
+ {
504
+ input: "foo {$FOOBAR} bar",
505
+ expect: "foo foobar bar",
506
+ },
507
+ {
508
+ input: "foo{$FOOBAR}bar",
509
+ expect: "foofoobarbar",
510
+ },
511
+ {
512
+ input: "foo\n{$FOOBAR}\nbar",
513
+ expect: "foo\nfoobar\nbar",
514
+ },
515
+ {
516
+ input: "{$FOOBAR} {$FOOBAR}",
517
+ expect: "foobar foobar",
518
+ },
519
+ {
520
+ input: "{$FOOBAR}{$FOOBAR}",
521
+ expect: "foobarfoobar",
522
+ },
523
+ {
524
+ input: "{$FOOBAR",
525
+ expect: "{$FOOBAR",
526
+ },
527
+ {
528
+ input: "{$LONGER_NAME $FOOBAR}",
529
+ expect: "",
530
+ },
531
+ {
532
+ input: "{$}",
533
+ expect: "{$}",
534
+ },
535
+ {
536
+ input: "{$$}",
537
+ expect: "",
538
+ },
539
+ {
540
+ input: "{$",
541
+ expect: "{$",
542
+ },
543
+ {
544
+ input: "}{$",
545
+ expect: "}{$",
546
+ },
547
+ } {
548
+ actual := replaceEnvVars([]byte(test.input))
549
+ if !bytes.Equal(actual, []byte(test.expect)) {
550
+ t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
551
+ }
552
}
553
}
554