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