From c5fffb4ac2631f0b41a8e13b62925b9dc8346cb9 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Fri, 18 Mar 2022 17:08:23 -0400 Subject: [PATCH] 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 --- caddyconfig/caddyfile/dispenser.go | 63 ++++++++++ caddyconfig/caddyfile/lexer.go | 12 +- .../caddyfile_adapt/expression_quotes.txt | 114 ++++++++++++++++++ .../caddyfile_adapt/map_with_raw_types.txt | 107 ++++++++++++++++ modules/caddyhttp/celmatcher.go | 6 +- modules/caddyhttp/map/caddyfile.go | 21 +--- 6 files changed, 300 insertions(+), 23 deletions(-) create mode 100644 caddytest/integration/caddyfile_adapt/expression_quotes.txt create mode 100644 caddytest/integration/caddyfile_adapt/map_with_raw_types.txt diff --git a/caddyconfig/caddyfile/dispenser.go b/caddyconfig/caddyfile/dispenser.go index 23f6eade..fbe71ad0 100755 --- a/caddyconfig/caddyfile/dispenser.go +++ b/caddyconfig/caddyfile/dispenser.go @@ -19,6 +19,7 @@ import ( "fmt" "io" "log" + "strconv" "strings" ) @@ -201,6 +202,43 @@ func (d *Dispenser) Val() string { return d.tokens[d.cursor].Text } +// ValRaw gets the raw text of the current token (including quotes). +// If there is no token loaded, it returns empty string. +func (d *Dispenser) ValRaw() string { + if d.cursor < 0 || d.cursor >= len(d.tokens) { + return "" + } + quote := d.tokens[d.cursor].wasQuoted + if quote > 0 { + return string(quote) + d.tokens[d.cursor].Text + string(quote) // string literal + } + return d.tokens[d.cursor].Text +} + +// ScalarVal gets value of the current token, converted to the closest +// scalar type. If there is no token loaded, it returns nil. +func (d *Dispenser) ScalarVal() interface{} { + if d.cursor < 0 || d.cursor >= len(d.tokens) { + return nil + } + quote := d.tokens[d.cursor].wasQuoted + text := d.tokens[d.cursor].Text + + if quote > 0 { + return text // string literal + } + if num, err := strconv.Atoi(text); err == nil { + return num + } + if num, err := strconv.ParseFloat(text, 64); err == nil { + return num + } + if bool, err := strconv.ParseBool(text); err == nil { + return bool + } + return text +} + // Line gets the line number of the current token. // If there is no token loaded, it returns 0. func (d *Dispenser) Line() int { @@ -249,6 +287,19 @@ func (d *Dispenser) AllArgs(targets ...*string) bool { return true } +// CountRemainingArgs counts the amount of remaining arguments +// (tokens on the same line) without consuming the tokens. +func (d *Dispenser) CountRemainingArgs() int { + count := 0 + for d.NextArg() { + count++ + } + for i := 0; i < count; i++ { + d.Prev() + } + return count +} + // RemainingArgs loads any more arguments (tokens on the same line) // into a slice and returns them. Open curly brace tokens also indicate // the end of arguments, and the curly brace is not included in @@ -261,6 +312,18 @@ func (d *Dispenser) RemainingArgs() []string { return args } +// RemainingArgsRaw loads any more arguments (tokens on the same line, +// retaining quotes) into a slice and returns them. Open curly brace +// tokens also indicate the end of arguments, and the curly brace is +// not included in the return value nor is it loaded. +func (d *Dispenser) RemainingArgsRaw() []string { + var args []string + for d.NextArg() { + args = append(args, d.ValRaw()) + } + return args +} + // NewFromNextSegment returns a new dispenser with a copy of // the tokens from the current token until the end of the // "directive" whether that be to the end of the line or diff --git a/caddyconfig/caddyfile/lexer.go b/caddyconfig/caddyfile/lexer.go index 968277f4..4a23524d 100755 --- a/caddyconfig/caddyfile/lexer.go +++ b/caddyconfig/caddyfile/lexer.go @@ -38,6 +38,7 @@ type ( File string Line int Text string + wasQuoted rune // enclosing quote character, if any inSnippet bool snippetName string } @@ -78,8 +79,9 @@ func (l *lexer) next() bool { var val []rune var comment, quoted, btQuoted, escaped bool - makeToken := func() bool { + makeToken := func(quoted rune) bool { l.token.Text = string(val) + l.token.wasQuoted = quoted return true } @@ -87,7 +89,7 @@ func (l *lexer) next() bool { ch, _, err := l.reader.ReadRune() if err != nil { if len(val) > 0 { - return makeToken() + return makeToken(0) } if err == io.EOF { return false @@ -110,10 +112,10 @@ func (l *lexer) next() bool { escaped = false } else { if quoted && ch == '"' { - return makeToken() + return makeToken('"') } if btQuoted && ch == '`' { - return makeToken() + return makeToken('`') } } if ch == '\n' { @@ -139,7 +141,7 @@ func (l *lexer) next() bool { comment = false } if len(val) > 0 { - return makeToken() + return makeToken(0) } continue } diff --git a/caddytest/integration/caddyfile_adapt/expression_quotes.txt b/caddytest/integration/caddyfile_adapt/expression_quotes.txt new file mode 100644 index 00000000..f5f8983e --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/expression_quotes.txt @@ -0,0 +1,114 @@ +example.com + +@a expression {http.error.status_code} == 400 +abort @a + +@b expression {http.error.status_code} == "401" +abort @b + +@c expression {http.error.status_code} == `402` +abort @c + +@d expression "{http.error.status_code} == 403" +abort @d + +@e expression `{http.error.status_code} == 404` +abort @e +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "abort": true, + "handler": "static_response" + } + ], + "match": [ + { + "expression": "{http.error.status_code} == 400" + } + ] + }, + { + "handle": [ + { + "abort": true, + "handler": "static_response" + } + ], + "match": [ + { + "expression": "{http.error.status_code} == \"401\"" + } + ] + }, + { + "handle": [ + { + "abort": true, + "handler": "static_response" + } + ], + "match": [ + { + "expression": "{http.error.status_code} == `402`" + } + ] + }, + { + "handle": [ + { + "abort": true, + "handler": "static_response" + } + ], + "match": [ + { + "expression": "{http.error.status_code} == 403" + } + ] + }, + { + "handle": [ + { + "abort": true, + "handler": "static_response" + } + ], + "match": [ + { + "expression": "{http.error.status_code} == 404" + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/map_with_raw_types.txt b/caddytest/integration/caddyfile_adapt/map_with_raw_types.txt new file mode 100644 index 00000000..54b2b60c --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/map_with_raw_types.txt @@ -0,0 +1,107 @@ +example.com + +map {host} {my_placeholder} {magic_number} { + # Should output boolean "true" and an integer + example.com true 3 + + # Should output a string and null + foo.example.com "string value" + + # Should output two strings (quoted int) + (.*)\.example.com "${1} subdomain" "5" + + # Should output null and a string (quoted int) + ~.*\.net$ - `7` + + # Should output a float and the string "false" + ~.*\.xyz$ 123.456 "false" + + # Should output two strings, second being escaped quote + default "unknown domain" \""" +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "defaults": [ + "unknown domain", + "\"" + ], + "destinations": [ + "{my_placeholder}", + "{magic_number}" + ], + "handler": "map", + "mappings": [ + { + "input": "example.com", + "outputs": [ + true, + 3 + ] + }, + { + "input": "foo.example.com", + "outputs": [ + "string value", + null + ] + }, + { + "input": "(.*)\\.example.com", + "outputs": [ + "${1} subdomain", + "5" + ] + }, + { + "input_regexp": ".*\\.net$", + "outputs": [ + null, + "7" + ] + }, + { + "input_regexp": ".*\\.xyz$", + "outputs": [ + 123.456, + "false" + ] + } + ], + "source": "{http.request.host}" + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go index d7d55d84..0306f390 100644 --- a/modules/caddyhttp/celmatcher.go +++ b/modules/caddyhttp/celmatcher.go @@ -150,7 +150,11 @@ func (m MatchExpression) Match(r *http.Request) bool { // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchExpression) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { - m.Expr = strings.Join(d.RemainingArgs(), " ") + if d.CountRemainingArgs() > 1 { + m.Expr = strings.Join(d.RemainingArgsRaw(), " ") + } else { + m.Expr = d.Val() + } } return nil } diff --git a/modules/caddyhttp/map/caddyfile.go b/modules/caddyhttp/map/caddyfile.go index a7f809b0..8394b211 100644 --- a/modules/caddyhttp/map/caddyfile.go +++ b/modules/caddyhttp/map/caddyfile.go @@ -15,7 +15,6 @@ package maphandler import ( - "strconv" "strings" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" @@ -75,11 +74,12 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) // every other line maps one input to one or more outputs in := h.Val() var outs []interface{} - for _, out := range h.RemainingArgs() { - if out == "-" { + for h.NextArg() { + val := h.ScalarVal() + if val == "-" { outs = append(outs, nil) } else { - outs = append(outs, specificType(out)) + outs = append(outs, val) } } @@ -108,16 +108,3 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) return handler, nil } - -func specificType(v string) interface{} { - if num, err := strconv.Atoi(v); err == nil { - return num - } - if num, err := strconv.ParseFloat(v, 64); err == nil { - return num - } - if bool, err := strconv.ParseBool(v); err == nil { - return bool - } - return v -}