Skip to content

Commit

Permalink
caddyfile: Support for raw token values; improve map, expression (#…
Browse files Browse the repository at this point in the history
…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>
  • Loading branch information
francislavoie and mholt committed Mar 18, 2022
1 parent dc4d147 commit c5fffb4
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 23 deletions.
63 changes: 63 additions & 0 deletions caddyconfig/caddyfile/dispenser.go
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"
"io"
"log"
"strconv"
"strings"
)

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
12 changes: 7 additions & 5 deletions caddyconfig/caddyfile/lexer.go
Expand Up @@ -38,6 +38,7 @@ type (
File string
Line int
Text string
wasQuoted rune // enclosing quote character, if any
inSnippet bool
snippetName string
}
Expand Down Expand Up @@ -78,16 +79,17 @@ 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
}

for {
ch, _, err := l.reader.ReadRune()
if err != nil {
if len(val) > 0 {
return makeToken()
return makeToken(0)
}
if err == io.EOF {
return false
Expand All @@ -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' {
Expand All @@ -139,7 +141,7 @@ func (l *lexer) next() bool {
comment = false
}
if len(val) > 0 {
return makeToken()
return makeToken(0)
}
continue
}
Expand Down
114 changes: 114 additions & 0 deletions 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
}
]
}
}
}
}
}
107 changes: 107 additions & 0 deletions 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
}
]
}
}
}
}
}
6 changes: 5 additions & 1 deletion modules/caddyhttp/celmatcher.go
Expand Up @@ -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
}
Expand Down

0 comments on commit c5fffb4

Please sign in to comment.