diff --git a/caddyconfig/caddyfile/dispenser.go b/caddyconfig/caddyfile/dispenser.go index 23f6eade7999..6a65e2bf457e 100755 --- a/caddyconfig/caddyfile/dispenser.go +++ b/caddyconfig/caddyfile/dispenser.go @@ -201,6 +201,19 @@ func (d *Dispenser) Val() string { return d.tokens[d.cursor].Text } +// RawVal gets the raw text of the current token (including quotes). +// If there is no token loaded, it returns empty string. +func (d *Dispenser) RawVal() 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) + } + return d.tokens[d.cursor].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 +262,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 +287,18 @@ func (d *Dispenser) RemainingArgs() []string { return args } +// RawRemainingArgs 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) RawRemainingArgs() []string { + var args []string + for d.NextArg() { + args = append(args, d.RawVal()) + } + 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 968277f48a32..afe84b0ba4b7 100755 --- a/caddyconfig/caddyfile/lexer.go +++ b/caddyconfig/caddyfile/lexer.go @@ -38,6 +38,7 @@ type ( File string Line int Text string + wasQuoted rune 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(-1) } 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(-1) } continue } diff --git a/caddytest/integration/caddyfile_adapt/expression_quotes.txt b/caddytest/integration/caddyfile_adapt/expression_quotes.txt new file mode 100644 index 000000000000..f5f8983eb95c --- /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 000000000000..54b2b60c85ba --- /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 d7d55d84b1e2..7e496656d0c2 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.RawRemainingArgs(), " ") + } else { + m.Expr = d.Val() + } } return nil } diff --git a/modules/caddyhttp/map/caddyfile.go b/modules/caddyhttp/map/caddyfile.go index a7f809b00854..4273f2c60661 100644 --- a/modules/caddyhttp/map/caddyfile.go +++ b/modules/caddyhttp/map/caddyfile.go @@ -75,7 +75,7 @@ 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() { + for _, out := range h.RawRemainingArgs() { if out == "-" { outs = append(outs, nil) } else { @@ -109,7 +109,17 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) return handler, nil } +// specificType parses the token string and casts it to +// the appropriate scalar type. Supports " and ` quoted +// strings, integers, floats, bool (true/false), and +// anything else is returned as a string. func specificType(v string) interface{} { + if strings.HasPrefix(v, "\"") { + return strings.Trim(v, "\"") + } + if strings.HasPrefix(v, "`") { + return strings.Trim(v, "`") + } if num, err := strconv.Atoi(v); err == nil { return num }