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
+}