@marcel91200 / caddy Mirror · read-only / commits / 7c419d53
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 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