From 7c419d5349837b50c9d87be88fc438f8c4e475b9 Mon Sep 17 00:00:00 2001 From: Mark Sargent <99003+sarge@users.noreply.github.com> Date: Fri, 10 Jan 2020 05:40:16 +1300 Subject: 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 --- caddyconfig/caddyfile/parse.go | 86 ++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 37 deletions(-) (limited to 'caddyconfig/caddyfile/parse.go') diff --git a/caddyconfig/caddyfile/parse.go b/caddyconfig/caddyfile/parse.go index 1862ad1..5792cf8 100755 --- a/caddyconfig/caddyfile/parse.go +++ b/caddyconfig/caddyfile/parse.go @@ -15,6 +15,7 @@ package caddyfile import ( + "bytes" "io" "log" "os" @@ -28,8 +29,12 @@ import ( // Directives that do not appear in validDirectives will cause // an error. If you do not want to check for valid directives, // pass in nil instead. -func Parse(filename string, input io.Reader) ([]ServerBlock, error) { - tokens, err := allTokens(filename, input) +// +// Environment variables in {$ENVIRONMENT_VARIABLE} notation +// will be replaced before parsing begins. +func Parse(filename string, input []byte) ([]ServerBlock, error) { + input = replaceEnvVars(input) + tokens, err := allTokens(filename, bytes.NewReader(input)) if err != nil { return nil, err } @@ -37,6 +42,41 @@ func Parse(filename string, input io.Reader) ([]ServerBlock, error) { return p.parseAll() } +// replaceEnvVars replaces all occurrences of environment variables. +func replaceEnvVars(input []byte) []byte { + var offset int + for { + begin := bytes.Index(input[offset:], spanOpen) + if begin < 0 { + break + } + begin += offset // make beginning relative to input, not offset + end := bytes.Index(input[begin+len(spanOpen):], spanClose) + if end < 0 { + break + } + end += begin + len(spanOpen) // make end relative to input, not begin + + // get the name; if there is no name, skip it + envVarName := input[begin+len(spanOpen) : end] + if len(envVarName) == 0 { + offset = end + len(spanClose) + continue + } + + // get the value of the environment variable + envVarValue := []byte(os.Getenv(string(envVarName))) + + // splice in the value + input = append(input[:begin], + append(envVarValue, input[end+len(spanClose):]...)...) + + // continue at the end of the replacement + offset = begin + len(envVarValue) + } + return input +} + // allTokens lexes the entire input, but does not parse it. // It returns all the tokens from the input, unstructured // and in order. @@ -128,7 +168,7 @@ func (p *parser) addresses() error { var expectingAnother bool for { - tkn := replaceEnvVars(p.Val()) + tkn := p.Val() // special case: import directive replaces tokens during parse-time if tkn == "import" && p.isNewLine() { @@ -245,7 +285,7 @@ func (p *parser) doImport() error { if !p.NextArg() { return p.ArgErr() } - importPattern := replaceEnvVars(p.Val()) + importPattern := p.Val() if importPattern == "" { return p.Err("Import requires a non-empty filepath") } @@ -353,8 +393,6 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) { // are loaded into the current server block for later use // by directive setup functions. func (p *parser) directive() error { - // evaluate any env vars in directive token - p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text) // a segment is a list of tokens associated with this directive var segment Segment @@ -379,7 +417,7 @@ func (p *parser) directive() error { p.cursor-- // cursor is advanced when we continue, so roll back one more continue } - p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text) + segment = append(segment, p.Token()) } @@ -414,36 +452,6 @@ func (p *parser) closeCurlyBrace() error { return nil } -// replaceEnvVars replaces environment variables that appear in the token -// and understands both the $UNIX and %WINDOWS% syntaxes. -func replaceEnvVars(s string) string { - s = replaceEnvReferences(s, "{%", "%}") - s = replaceEnvReferences(s, "{$", "}") - return s -} - -// replaceEnvReferences performs the actual replacement of env variables -// in s, given the placeholder start and placeholder end strings. -func replaceEnvReferences(s, refStart, refEnd string) string { - index := strings.Index(s, refStart) - for index != -1 { - endIndex := strings.Index(s[index:], refEnd) - if endIndex == -1 { - break - } - - endIndex += index - if endIndex > index+len(refStart) { - ref := s[index : endIndex+len(refEnd)] - s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1) - } else { - return s - } - index = strings.Index(s, refStart) - } - return s -} - func (p *parser) isSnippet() (bool, string) { keys := p.block.Keys // A snippet block is a single key with parens. Nothing else qualifies. @@ -514,3 +522,7 @@ func (s Segment) Directive() string { } return "" } + +// spanOpen and spanClose are used to bound spans that +// contain the name of an environment variable. +var spanOpen, spanClose = []byte{'{', '$'}, []byte{'}'} -- cgit v1.2.3