@marcel91200 / caddy Mirror · read-only / commits / 30b6d1f4
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 13, 19:15 UTC

cmd: Enhance .env (dotenv) file parsing

Basic support for quoted values, newlines in quoted values, and comments. Does not support variable or command expansion.

Matthew Holt committed Apr 13, 2022 at 11:35 UTC 30b6d1f47a35d00fca7cff0daa2ff59a98c5a85e
2 files changed +212 -16
cmd/main.go
+42 -16
@@ -368,42 +368,68 @@ func loadEnvFromFile(envFile string) error {
368 return nil
369 }
370
371 +// parseEnvFile parses an env file from KEY=VALUE format.
372 +// It's pretty naive. Limited value quotation is supported,
373 +// but variable and command expansions are not supported.
374 func parseEnvFile(envInput io.Reader) (map[string]string, error) {
375 envMap := make(map[string]string)
376
377 scanner := bufio.NewScanner(envInput)
375 - var line string
376 - lineNumber := 0
378 + var lineNumber int
379
380 for scanner.Scan() {
379 - line = strings.TrimSpace(scanner.Text())
381 + line := strings.TrimSpace(scanner.Text())
382 lineNumber++
383
382 - // skip lines starting with comment
383 - if strings.HasPrefix(line, "#") {
384 - continue
385 - }
386 -
387 - // skip empty line
388 - if len(line) == 0 {
384 + // skip empty lines and lines starting with comment
385 + if line == "" || strings.HasPrefix(line, "#") {
386 continue
387 }
388
389 + // split line into key and value
390 fields := strings.SplitN(line, "=", 2)
391 if len(fields) != 2 {
392 return nil, fmt.Errorf("can't parse line %d; line should be in KEY=VALUE format", lineNumber)
393 }
394 + key, val := fields[0], fields[1]
395
397 - if strings.Contains(fields[0], " ") {
398 - return nil, fmt.Errorf("bad key on line %d: contains whitespace", lineNumber)
399 - }
400 -
401 - key := fields[0]
402 - val := fields[1]
396 + // sometimes keys are prefixed by "export " so file can be sourced in bash; ignore it here
397 + key = strings.TrimPrefix(key, "export ")
398
399 + // validate key and value
400 if key == "" {
401 return nil, fmt.Errorf("missing or empty key on line %d", lineNumber)
402 }
403 + if strings.Contains(key, " ") {
404 + return nil, fmt.Errorf("invalid key on line %d: contains whitespace: %s", lineNumber, key)
405 + }
406 + if strings.HasPrefix(val, " ") || strings.HasPrefix(val, "\t") {
407 + return nil, fmt.Errorf("invalid value on line %d: whitespace before value: '%s'", lineNumber, val)
408 + }
409 +
410 + // remove any trailing comment after value
411 + if commentStart := strings.Index(val, "#"); commentStart > 0 {
412 + before := val[commentStart-1]
413 + if before == '\t' || before == ' ' {
414 + val = strings.TrimRight(val[:commentStart], " \t")
415 + }
416 + }
417 +
418 + // quoted value: support newlines
419 + if strings.HasPrefix(val, `"`) {
420 + for !(strings.HasSuffix(line, `"`) && !strings.HasSuffix(line, `\"`)) {
421 + val = strings.ReplaceAll(val, `\"`, `"`)
422 + if !scanner.Scan() {
423 + break
424 + }
425 + lineNumber++
426 + line = strings.ReplaceAll(scanner.Text(), `\"`, `"`)
427 + val += "\n" + line
428 + }
429 + val = strings.TrimPrefix(val, `"`)
430 + val = strings.TrimSuffix(val, `"`)
431 + }
432 +
433 envMap[key] = val
434 }
435
cmd/main_test.go new
+170
@@ -0,0 +1,170 @@
1 +package caddycmd
2 +
3 +import (
4 + "reflect"
5 + "strings"
6 + "testing"
7 +)
8 +
9 +func TestParseEnvFile(t *testing.T) {
10 + for i, tc := range []struct {
11 + input string
12 + expect map[string]string
13 + shouldErr bool
14 + }{
15 + {
16 + input: `KEY=value`,
17 + expect: map[string]string{
18 + "KEY": "value",
19 + },
20 + },
21 + {
22 + input: `
23 + KEY=value
24 + OTHER_KEY=Some Value
25 + `,
26 + expect: map[string]string{
27 + "KEY": "value",
28 + "OTHER_KEY": "Some Value",
29 + },
30 + },
31 + {
32 + input: `
33 + KEY=value
34 + INVALID KEY=asdf
35 + OTHER_KEY=Some Value
36 + `,
37 + shouldErr: true,
38 + },
39 + {
40 + input: `
41 + KEY=value
42 + SIMPLE_QUOTED="quoted value"
43 + OTHER_KEY=Some Value
44 + `,
45 + expect: map[string]string{
46 + "KEY": "value",
47 + "SIMPLE_QUOTED": "quoted value",
48 + "OTHER_KEY": "Some Value",
49 + },
50 + },
51 + {
52 + input: `
53 + KEY=value
54 + NEWLINES="foo
55 + bar"
56 + OTHER_KEY=Some Value
57 + `,
58 + expect: map[string]string{
59 + "KEY": "value",
60 + "NEWLINES": "foo\n\tbar",
61 + "OTHER_KEY": "Some Value",
62 + },
63 + },
64 + {
65 + input: `
66 + KEY=value
67 + ESCAPED="\"escaped quotes\"
68 +here"
69 + OTHER_KEY=Some Value
70 + `,
71 + expect: map[string]string{
72 + "KEY": "value",
73 + "ESCAPED": "\"escaped quotes\"\nhere",
74 + "OTHER_KEY": "Some Value",
75 + },
76 + },
77 + {
78 + input: `
79 + export KEY=value
80 + OTHER_KEY=Some Value
81 + `,
82 + expect: map[string]string{
83 + "KEY": "value",
84 + "OTHER_KEY": "Some Value",
85 + },
86 + },
87 + {
88 + input: `
89 + =value
90 + OTHER_KEY=Some Value
91 + `,
92 + shouldErr: true,
93 + },
94 + {
95 + input: `
96 + EMPTY=
97 + OTHER_KEY=Some Value
98 + `,
99 + expect: map[string]string{
100 + "EMPTY": "",
101 + "OTHER_KEY": "Some Value",
102 + },
103 + },
104 + {
105 + input: `
106 + EMPTY=""
107 + OTHER_KEY=Some Value
108 + `,
109 + expect: map[string]string{
110 + "EMPTY": "",
111 + "OTHER_KEY": "Some Value",
112 + },
113 + },
114 + {
115 + input: `
116 + KEY=value
117 + #OTHER_KEY=Some Value
118 + `,
119 + expect: map[string]string{
120 + "KEY": "value",
121 + },
122 + },
123 + {
124 + input: `
125 + KEY=value
126 + COMMENT=foo bar # some comment here
127 + OTHER_KEY=Some Value
128 + `,
129 + expect: map[string]string{
130 + "KEY": "value",
131 + "COMMENT": "foo bar",
132 + "OTHER_KEY": "Some Value",
133 + },
134 + },
135 + {
136 + input: `
137 + KEY=value
138 + WHITESPACE= foo
139 + OTHER_KEY=Some Value
140 + `,
141 + shouldErr: true,
142 + },
143 + {
144 + input: `
145 + KEY=value
146 + WHITESPACE=" foo bar "
147 + OTHER_KEY=Some Value
148 + `,
149 + expect: map[string]string{
150 + "KEY": "value",
151 + "WHITESPACE": " foo bar ",
152 + "OTHER_KEY": "Some Value",
153 + },
154 + },
155 + } {
156 + actual, err := parseEnvFile(strings.NewReader(tc.input))
157 + if err != nil && !tc.shouldErr {
158 + t.Errorf("Test %d: Got error but shouldn't have: %v", i, err)
159 + }
160 + if err == nil && tc.shouldErr {
161 + t.Errorf("Test %d: Did not get error but should have", i)
162 + }
163 + if tc.shouldErr {
164 + continue
165 + }
166 + if !reflect.DeepEqual(tc.expect, actual) {
167 + t.Errorf("Test %d: Expected %v but got %v", i, tc.expect, actual)
168 + }
169 + }
170 +}