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, 20:30 UTC
caddyfile: Support for raw token values; improve `map`, `expression` (#4643)
* caddyfile: Support for raw token values, improve `map`, `expression` * Applied code review comments * Rename RawVal to ValRaw Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
Francis Lavoie committed
Mar 18, 2022 at 17:08 UTC
c5fffb4ac2631f0b41a8e13b62925b9dc8346cb9
6 files changed
+300
-23
caddyconfig/caddyfile/dispenser.go
+63
@@ -19,6 +19,7 @@ import (
19
"fmt"
20
"io"
21
"log"
22
+ "strconv"
23
"strings"
24
)
25
@@ -201,6 +202,43 @@ func (d *Dispenser) Val() string {
202
return d.tokens[d.cursor].Text
203
}
204
205
+// ValRaw gets the raw text of the current token (including quotes).
206
+// If there is no token loaded, it returns empty string.
207
+func (d *Dispenser) ValRaw() string {
208
+ if d.cursor < 0 || d.cursor >= len(d.tokens) {
209
+ return ""
210
+ }
211
+ quote := d.tokens[d.cursor].wasQuoted
212
+ if quote > 0 {
213
+ return string(quote) + d.tokens[d.cursor].Text + string(quote) // string literal
214
+ }
215
+ return d.tokens[d.cursor].Text
216
+}
217
+
218
+// ScalarVal gets value of the current token, converted to the closest
219
+// scalar type. If there is no token loaded, it returns nil.
220
+func (d *Dispenser) ScalarVal() interface{} {
221
+ if d.cursor < 0 || d.cursor >= len(d.tokens) {
222
+ return nil
223
+ }
224
+ quote := d.tokens[d.cursor].wasQuoted
225
+ text := d.tokens[d.cursor].Text
226
+
227
+ if quote > 0 {
228
+ return text // string literal
229
+ }
230
+ if num, err := strconv.Atoi(text); err == nil {
231
+ return num
232
+ }
233
+ if num, err := strconv.ParseFloat(text, 64); err == nil {
234
+ return num
235
+ }
236
+ if bool, err := strconv.ParseBool(text); err == nil {
237
+ return bool
238
+ }
239
+ return text
240
+}
241
+
242
// Line gets the line number of the current token.
243
// If there is no token loaded, it returns 0.
244
func (d *Dispenser) Line() int {
@@ -249,6 +287,19 @@ func (d *Dispenser) AllArgs(targets ...*string) bool {
287
return true
288
}
289
290
+// CountRemainingArgs counts the amount of remaining arguments
291
+// (tokens on the same line) without consuming the tokens.
292
+func (d *Dispenser) CountRemainingArgs() int {
293
+ count := 0
294
+ for d.NextArg() {
295
+ count++
296
+ }
297
+ for i := 0; i < count; i++ {
298
+ d.Prev()
299
+ }
300
+ return count
301
+}
302
+
303
// RemainingArgs loads any more arguments (tokens on the same line)
304
// into a slice and returns them. Open curly brace tokens also indicate
305
// the end of arguments, and the curly brace is not included in
@@ -261,6 +312,18 @@ func (d *Dispenser) RemainingArgs() []string {
312
return args
313
}
314
315
+// RemainingArgsRaw loads any more arguments (tokens on the same line,
316
+// retaining quotes) into a slice and returns them. Open curly brace
317
+// tokens also indicate the end of arguments, and the curly brace is
318
+// not included in the return value nor is it loaded.
319
+func (d *Dispenser) RemainingArgsRaw() []string {
320
+ var args []string
321
+ for d.NextArg() {
322
+ args = append(args, d.ValRaw())
323
+ }
324
+ return args
325
+}
326
+
327
// NewFromNextSegment returns a new dispenser with a copy of
328
// the tokens from the current token until the end of the
329
// "directive" whether that be to the end of the line or
caddyconfig/caddyfile/lexer.go
+7
-5
@@ -38,6 +38,7 @@ type (
38
File string
39
Line int
40
Text string
41
+ wasQuoted rune // enclosing quote character, if any
42
inSnippet bool
43
snippetName string
44
}
@@ -78,8 +79,9 @@ func (l *lexer) next() bool {
79
var val []rune
80
var comment, quoted, btQuoted, escaped bool
81
81
- makeToken := func() bool {
82
+ makeToken := func(quoted rune) bool {
83
l.token.Text = string(val)
84
+ l.token.wasQuoted = quoted
85
return true
86
}
87
@@ -87,7 +89,7 @@ func (l *lexer) next() bool {
89
ch, _, err := l.reader.ReadRune()
90
if err != nil {
91
if len(val) > 0 {
90
- return makeToken()
92
+ return makeToken(0)
93
}
94
if err == io.EOF {
95
return false
@@ -110,10 +112,10 @@ func (l *lexer) next() bool {
112
escaped = false
113
} else {
114
if quoted && ch == '"' {
113
- return makeToken()
115
+ return makeToken('"')
116
}
117
if btQuoted && ch == '`' {
116
- return makeToken()
118
+ return makeToken('`')
119
}
120
}
121
if ch == '\n' {
@@ -139,7 +141,7 @@ func (l *lexer) next() bool {
141
comment = false
142
}
143
if len(val) > 0 {
142
- return makeToken()
144
+ return makeToken(0)
145
}
146
continue
147
}
caddytest/integration/caddyfile_adapt/expression_quotes.txt
new
+114
@@ -0,0 +1,114 @@
1
+example.com
2
+
3
+@a expression {http.error.status_code} == 400
4
+abort @a
5
+
6
+@b expression {http.error.status_code} == "401"
7
+abort @b
8
+
9
+@c expression {http.error.status_code} == `402`
10
+abort @c
11
+
12
+@d expression "{http.error.status_code} == 403"
13
+abort @d
14
+
15
+@e expression `{http.error.status_code} == 404`
16
+abort @e
17
+----------
18
+{
19
+ "apps": {
20
+ "http": {
21
+ "servers": {
22
+ "srv0": {
23
+ "listen": [
24
+ ":443"
25
+ ],
26
+ "routes": [
27
+ {
28
+ "match": [
29
+ {
30
+ "host": [
31
+ "example.com"
32
+ ]
33
+ }
34
+ ],
35
+ "handle": [
36
+ {
37
+ "handler": "subroute",
38
+ "routes": [
39
+ {
40
+ "handle": [
41
+ {
42
+ "abort": true,
43
+ "handler": "static_response"
44
+ }
45
+ ],
46
+ "match": [
47
+ {
48
+ "expression": "{http.error.status_code} == 400"
49
+ }
50
+ ]
51
+ },
52
+ {
53
+ "handle": [
54
+ {
55
+ "abort": true,
56
+ "handler": "static_response"
57
+ }
58
+ ],
59
+ "match": [
60
+ {
61
+ "expression": "{http.error.status_code} == \"401\""
62
+ }
63
+ ]
64
+ },
65
+ {
66
+ "handle": [
67
+ {
68
+ "abort": true,
69
+ "handler": "static_response"
70
+ }
71
+ ],
72
+ "match": [
73
+ {
74
+ "expression": "{http.error.status_code} == `402`"
75
+ }
76
+ ]
77
+ },
78
+ {
79
+ "handle": [
80
+ {
81
+ "abort": true,
82
+ "handler": "static_response"
83
+ }
84
+ ],
85
+ "match": [
86
+ {
87
+ "expression": "{http.error.status_code} == 403"
88
+ }
89
+ ]
90
+ },
91
+ {
92
+ "handle": [
93
+ {
94
+ "abort": true,
95
+ "handler": "static_response"
96
+ }
97
+ ],
98
+ "match": [
99
+ {
100
+ "expression": "{http.error.status_code} == 404"
101
+ }
102
+ ]
103
+ }
104
+ ]
105
+ }
106
+ ],
107
+ "terminal": true
108
+ }
109
+ ]
110
+ }
111
+ }
112
+ }
113
+ }
114
+}
\ No newline at end of file
caddytest/integration/caddyfile_adapt/map_with_raw_types.txt
new
+107
@@ -0,0 +1,107 @@
1
+example.com
2
+
3
+map {host} {my_placeholder} {magic_number} {
4
+ # Should output boolean "true" and an integer
5
+ example.com true 3
6
+
7
+ # Should output a string and null
8
+ foo.example.com "string value"
9
+
10
+ # Should output two strings (quoted int)
11
+ (.*)\.example.com "${1} subdomain" "5"
12
+
13
+ # Should output null and a string (quoted int)
14
+ ~.*\.net$ - `7`
15
+
16
+ # Should output a float and the string "false"
17
+ ~.*\.xyz$ 123.456 "false"
18
+
19
+ # Should output two strings, second being escaped quote
20
+ default "unknown domain" \"""
21
+}
22
+----------
23
+{
24
+ "apps": {
25
+ "http": {
26
+ "servers": {
27
+ "srv0": {
28
+ "listen": [
29
+ ":443"
30
+ ],
31
+ "routes": [
32
+ {
33
+ "match": [
34
+ {
35
+ "host": [
36
+ "example.com"
37
+ ]
38
+ }
39
+ ],
40
+ "handle": [
41
+ {
42
+ "handler": "subroute",
43
+ "routes": [
44
+ {
45
+ "handle": [
46
+ {
47
+ "defaults": [
48
+ "unknown domain",
49
+ "\""
50
+ ],
51
+ "destinations": [
52
+ "{my_placeholder}",
53
+ "{magic_number}"
54
+ ],
55
+ "handler": "map",
56
+ "mappings": [
57
+ {
58
+ "input": "example.com",
59
+ "outputs": [
60
+ true,
61
+ 3
62
+ ]
63
+ },
64
+ {
65
+ "input": "foo.example.com",
66
+ "outputs": [
67
+ "string value",
68
+ null
69
+ ]
70
+ },
71
+ {
72
+ "input": "(.*)\\.example.com",
73
+ "outputs": [
74
+ "${1} subdomain",
75
+ "5"
76
+ ]
77
+ },
78
+ {
79
+ "input_regexp": ".*\\.net$",
80
+ "outputs": [
81
+ null,
82
+ "7"
83
+ ]
84
+ },
85
+ {
86
+ "input_regexp": ".*\\.xyz$",
87
+ "outputs": [
88
+ 123.456,
89
+ "false"
90
+ ]
91
+ }
92
+ ],
93
+ "source": "{http.request.host}"
94
+ }
95
+ ]
96
+ }
97
+ ]
98
+ }
99
+ ],
100
+ "terminal": true
101
+ }
102
+ ]
103
+ }
104
+ }
105
+ }
106
+ }
107
+}
\ No newline at end of file
modules/caddyhttp/celmatcher.go
+5
-1
@@ -150,7 +150,11 @@ func (m MatchExpression) Match(r *http.Request) bool {
150
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
151
func (m *MatchExpression) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
152
for d.Next() {
153
- m.Expr = strings.Join(d.RemainingArgs(), " ")
153
+ if d.CountRemainingArgs() > 1 {
154
+ m.Expr = strings.Join(d.RemainingArgsRaw(), " ")
155
+ } else {
156
+ m.Expr = d.Val()
157
+ }
158
}
159
return nil
160
}
modules/caddyhttp/map/caddyfile.go
+4
-17
@@ -15,7 +15,6 @@
15
package maphandler
16
17
import (
18
- "strconv"
18
"strings"
19
20
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
@@ -75,11 +74,12 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
74
// every other line maps one input to one or more outputs
75
in := h.Val()
76
var outs []interface{}
78
- for _, out := range h.RemainingArgs() {
79
- if out == "-" {
77
+ for h.NextArg() {
78
+ val := h.ScalarVal()
79
+ if val == "-" {
80
outs = append(outs, nil)
81
} else {
82
- outs = append(outs, specificType(out))
82
+ outs = append(outs, val)
83
}
84
}
85
@@ -108,16 +108,3 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
108
109
return handler, nil
110
}
111
-
112
-func specificType(v string) interface{} {
113
- if num, err := strconv.Atoi(v); err == nil {
114
- return num
115
- }
116
- if num, err := strconv.ParseFloat(v, 64); err == nil {
117
- return num
118
- }
119
- if bool, err := strconv.ParseBool(v); err == nil {
120
- return bool
121
- }
122
- return v
123
-}