@marcel91200 / caddy Mirror · read-only / commits / 551f7937
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 11, 22:15 UTC

caddyfile: Fix importing nested tokens for `{block}` (#7189)

Artur H. committed Aug 22, 2025 at 23:29 UTC 551f793700fe1550845c824470b623fd1aa03d36
6 files changed +214 -23
caddyconfig/caddyfile/dispenser.go
+18 -6
@@ -308,9 +308,9 @@ func (d *Dispenser) CountRemainingArgs() int {
308 }
309
310 // RemainingArgs loads any more arguments (tokens on the same line)
311 -// into a slice and returns them. Open curly brace tokens also indicate
312 -// the end of arguments, and the curly brace is not included in
313 -// the return value nor is it loaded.
311 +// into a slice of strings and returns them. Open curly brace tokens
312 +// also indicate the end of arguments, and the curly brace is not
313 +// included in the return value nor is it loaded.
314 func (d *Dispenser) RemainingArgs() []string {
315 var args []string
316 for d.NextArg() {
@@ -320,9 +320,9 @@ func (d *Dispenser) RemainingArgs() []string {
320 }
321
322 // RemainingArgsRaw loads any more arguments (tokens on the same line,
323 -// retaining quotes) into a slice and returns them. Open curly brace
324 -// tokens also indicate the end of arguments, and the curly brace is
325 -// not included in the return value nor is it loaded.
323 +// retaining quotes) into a slice of strings and returns them.
324 +// Open curly brace tokens also indicate the end of arguments,
325 +// and the curly brace is not included in the return value nor is it loaded.
326 func (d *Dispenser) RemainingArgsRaw() []string {
327 var args []string
328 for d.NextArg() {
@@ -331,6 +331,18 @@ func (d *Dispenser) RemainingArgsRaw() []string {
331 return args
332 }
333
334 +// RemainingArgsAsTokens loads any more arguments (tokens on the same line)
335 +// into a slice of Token-structs and returns them. Open curly brace tokens
336 +// also indicate the end of arguments, and the curly brace is not included
337 +// in the return value nor is it loaded.
338 +func (d *Dispenser) RemainingArgsAsTokens() []Token {
339 + var args []Token
340 + for d.NextArg() {
341 + args = append(args, d.Token())
342 + }
343 + return args
344 +}
345 +
346 // NewFromNextSegment returns a new dispenser with a copy of
347 // the tokens from the current token until the end of the
348 // "directive" whether that be to the end of the line or
caddyconfig/caddyfile/dispenser_test.go
+60
@@ -274,6 +274,66 @@ func TestDispenser_RemainingArgs(t *testing.T) {
274 }
275 }
276
277 +func TestDispenser_RemainingArgsAsTokens(t *testing.T) {
278 + input := `dir1 arg1 arg2 arg3
279 + dir2 arg4 arg5
280 + dir3 arg6 { arg7
281 + dir4`
282 + d := NewTestDispenser(input)
283 +
284 + d.Next() // dir1
285 +
286 + args := d.RemainingArgsAsTokens()
287 +
288 + tokenTexts := make([]string, 0, len(args))
289 + for _, arg := range args {
290 + tokenTexts = append(tokenTexts, arg.Text)
291 + }
292 +
293 + if expected := []string{"arg1", "arg2", "arg3"}; !reflect.DeepEqual(tokenTexts, expected) {
294 + t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
295 + }
296 +
297 + d.Next() // dir2
298 +
299 + args = d.RemainingArgsAsTokens()
300 +
301 + tokenTexts = tokenTexts[:0]
302 + for _, arg := range args {
303 + tokenTexts = append(tokenTexts, arg.Text)
304 + }
305 +
306 + if expected := []string{"arg4", "arg5"}; !reflect.DeepEqual(tokenTexts, expected) {
307 + t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
308 + }
309 +
310 + d.Next() // dir3
311 +
312 + args = d.RemainingArgsAsTokens()
313 + tokenTexts = tokenTexts[:0]
314 + for _, arg := range args {
315 + tokenTexts = append(tokenTexts, arg.Text)
316 + }
317 +
318 + if expected := []string{"arg6"}; !reflect.DeepEqual(tokenTexts, expected) {
319 + t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
320 + }
321 +
322 + d.Next() // {
323 + d.Next() // arg7
324 + d.Next() // dir4
325 +
326 + args = d.RemainingArgsAsTokens()
327 + tokenTexts = tokenTexts[:0]
328 + for _, arg := range args {
329 + tokenTexts = append(tokenTexts, arg.Text)
330 + }
331 +
332 + if len(args) != 0 {
333 + t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", []string{}, tokenTexts)
334 + }
335 +}
336 +
337 func TestDispenser_ArgErr_Err(t *testing.T) {
338 input := `dir1 {
339 }
caddyconfig/caddyfile/parse.go
+12 -17
@@ -379,28 +379,23 @@ func (p *parser) doImport(nesting int) error {
379 if len(blockTokens) > 0 {
380 // use such tokens to create a new dispenser, and then use it to parse each block
381 bd := NewDispenser(blockTokens)
382 +
383 + // one iteration processes one sub-block inside the import
384 for bd.Next() {
383 - // see if we can grab a key
384 - var currentMappingKey string
385 - if bd.Val() == "{" {
385 + currentMappingKey := bd.Val()
386 +
387 + if currentMappingKey == "{" {
388 return p.Err("anonymous blocks are not supported")
389 }
388 - currentMappingKey = bd.Val()
389 - currentMappingTokens := []Token{}
390 - // read all args until end of line / {
391 - if bd.NextArg() {
390 +
391 + // load up all arguments (if there even are any)
392 + currentMappingTokens := bd.RemainingArgsAsTokens()
393 +
394 + // load up the entire block
395 + for mappingNesting := bd.Nesting(); bd.NextBlock(mappingNesting); {
396 currentMappingTokens = append(currentMappingTokens, bd.Token())
393 - for bd.NextArg() {
394 - currentMappingTokens = append(currentMappingTokens, bd.Token())
395 - }
396 - // TODO(elee1766): we don't enter another mapping here because it's annoying to extract the { and } properly.
397 - // maybe someone can do that in the future
398 - } else {
399 - // attempt to enter a block and add tokens to the currentMappingTokens
400 - for mappingNesting := bd.Nesting(); bd.NextBlock(mappingNesting); {
401 - currentMappingTokens = append(currentMappingTokens, bd.Token())
402 - }
397 }
398 +
399 blockMapping[currentMappingKey] = currentMappingTokens
400 }
401 }
caddyconfig/caddyfile/parse_test.go
+46
@@ -18,6 +18,7 @@ import (
18 "bytes"
19 "os"
20 "path/filepath"
21 + "strings"
22 "testing"
23 )
24
@@ -884,6 +885,51 @@ func TestRejectsGlobalMatcher(t *testing.T) {
885 }
886 }
887
888 +func TestRejectAnonymousImportBlock(t *testing.T) {
889 + p := testParser(`
890 + (site) {
891 + http://{args[0]} https://{args[0]} {
892 + {block}
893 + }
894 + }
895 +
896 + import site test.domain {
897 + {
898 + header_up Host {host}
899 + header_up X-Real-IP {remote_host}
900 + }
901 + }
902 + `)
903 + _, err := p.parseAll()
904 + if err == nil {
905 + t.Fatal("Expected an error, but got nil")
906 + }
907 + expected := "anonymous blocks are not supported"
908 + if !strings.HasPrefix(err.Error(), "anonymous blocks are not supported") {
909 + t.Errorf("Expected error to start with '%s' but got '%v'", expected, err)
910 + }
911 +}
912 +
913 +func TestAcceptSiteImportWithBraces(t *testing.T) {
914 + p := testParser(`
915 + (site) {
916 + http://{args[0]} https://{args[0]} {
917 + {block}
918 + }
919 + }
920 +
921 + import site test.domain {
922 + reverse_proxy http://192.168.1.1:8080 {
923 + header_up Host {host}
924 + }
925 + }
926 + `)
927 + _, err := p.parseAll()
928 + if err != nil {
929 + t.Errorf("Expected error to be nil but got '%v'", err)
930 + }
931 +}
932 +
933 func testParser(input string) parser {
934 return parser{Dispenser: NewTestDispenser(input)}
935 }
caddytest/integration/caddyfile_adapt/import_block_anonymous.caddyfiletest new
+13
@@ -0,0 +1,13 @@
1 +(site) {
2 + http://{args[0]} https://{args[0]} {
3 + {block}
4 + }
5 +}
6 +import site test.domain {
7 + {
8 + header_up Host {host}
9 + header_up X-Real-IP {remote_host}
10 + }
11 +}
12 +----------
13 +anonymous blocks are not supported
\ No newline at end of file
caddytest/integration/caddyfile_adapt/import_block_with_site_block.caddyfiletest new
+65
@@ -0,0 +1,65 @@
1 +(site) {
2 + https://{args[0]} {
3 + {block}
4 + }
5 +}
6 +
7 +import site test.domain {
8 + reverse_proxy http://192.168.1.1:8080 {
9 + header_up Host {host}
10 + }
11 +}
12 +----------
13 +{
14 + "apps": {
15 + "http": {
16 + "servers": {
17 + "srv0": {
18 + "listen": [
19 + ":443"
20 + ],
21 + "routes": [
22 + {
23 + "match": [
24 + {
25 + "host": [
26 + "test.domain"
27 + ]
28 + }
29 + ],
30 + "handle": [
31 + {
32 + "handler": "subroute",
33 + "routes": [
34 + {
35 + "handle": [
36 + {
37 + "handler": "reverse_proxy",
38 + "headers": {
39 + "request": {
40 + "set": {
41 + "Host": [
42 + "{http.request.host}"
43 + ]
44 + }
45 + }
46 + },
47 + "upstreams": [
48 + {
49 + "dial": "192.168.1.1:8080"
50 + }
51 + ]
52 + }
53 + ]
54 + }
55 + ]
56 + }
57 + ],
58 + "terminal": true
59 + }
60 + ]
61 + }
62 + }
63 + }
64 + }
65 +}