Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
caddyfile: reject cyclic imports (#4022)
* caddyfile: reject recursive self-imports * caddyfile: detect and reject cyclic imports of snippets and files * caddyfile: do not be stickler about connected nodes not being connected already * caddyfile: include missing test artifacts of cyclic imports * address review comments
Mohammed Al Sahaf committed
Apr 9, 2021 at 18:06 UTC
3903642aa7cf711cb20f27958e7ee5b15fe2bbb3
8 files changed
+193
-5
caddyconfig/caddyfile/importgraph.go
new
+127
@@ -0,0 +1,127 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddyfile
16
+
17
+import (
18
+ "fmt"
19
+)
20
+
21
+type adjacency map[string][]string
22
+
23
+type importGraph struct {
24
+ nodes map[string]bool
25
+ edges adjacency
26
+}
27
+
28
+func (i *importGraph) addNode(name string) {
29
+ if i.nodes == nil {
30
+ i.nodes = make(map[string]bool)
31
+ }
32
+ if _, exists := i.nodes[name]; exists {
33
+ return
34
+ }
35
+ i.nodes[name] = true
36
+}
37
+func (i *importGraph) addNodes(names []string) {
38
+ for _, name := range names {
39
+ i.addNode(name)
40
+ }
41
+}
42
+
43
+func (i *importGraph) removeNode(name string) {
44
+ delete(i.nodes, name)
45
+}
46
+func (i *importGraph) removeNodes(names []string) {
47
+ for _, name := range names {
48
+ i.removeNode(name)
49
+ }
50
+}
51
+
52
+func (i *importGraph) addEdge(from, to string) error {
53
+ if !i.exists(from) || !i.exists(to) {
54
+ return fmt.Errorf("one of the nodes does not exist")
55
+ }
56
+
57
+ if i.willCycle(to, from) {
58
+ return fmt.Errorf("a cycle of imports exists between %s and %s", from, to)
59
+ }
60
+
61
+ if i.areConnected(from, to) {
62
+ // if connected, there's nothing to do
63
+ return nil
64
+ }
65
+
66
+ if i.nodes == nil {
67
+ i.nodes = make(map[string]bool)
68
+ }
69
+ if i.edges == nil {
70
+ i.edges = make(adjacency)
71
+ }
72
+
73
+ i.edges[from] = append(i.edges[from], to)
74
+ return nil
75
+}
76
+func (i *importGraph) addEdges(from string, tos []string) error {
77
+ for _, to := range tos {
78
+ err := i.addEdge(from, to)
79
+ if err != nil {
80
+ return err
81
+ }
82
+ }
83
+ return nil
84
+}
85
+
86
+func (i *importGraph) areConnected(from, to string) bool {
87
+ al, ok := i.edges[from]
88
+ if !ok {
89
+ return false
90
+ }
91
+ for _, v := range al {
92
+ if v == to {
93
+ return true
94
+ }
95
+ }
96
+ return false
97
+}
98
+
99
+func (i *importGraph) willCycle(from, to string) bool {
100
+ collector := make(map[string]bool)
101
+
102
+ var visit func(string)
103
+ visit = func(start string) {
104
+ if !collector[start] {
105
+ collector[start] = true
106
+ for _, v := range i.edges[start] {
107
+ visit(v)
108
+ }
109
+ }
110
+ }
111
+
112
+ for _, v := range i.edges[from] {
113
+ visit(v)
114
+ }
115
+ for k := range collector {
116
+ if to == k {
117
+ return true
118
+ }
119
+ }
120
+
121
+ return false
122
+}
123
+
124
+func (i *importGraph) exists(key string) bool {
125
+ _, exists := i.nodes[key]
126
+ return exists
127
+}
caddyconfig/caddyfile/lexer.go
+5
-3
@@ -35,9 +35,11 @@ type (
35
36
// Token represents a single parsable unit.
37
Token struct {
38
- File string
39
- Line int
40
- Text string
38
+ File string
39
+ Line int
40
+ Text string
41
+ inSnippet bool
42
+ snippetName string
43
}
44
)
45
caddyconfig/caddyfile/parse.go
+35
-2
@@ -16,6 +16,7 @@ package caddyfile
16
17
import (
18
"bytes"
19
+ "fmt"
20
"io/ioutil"
21
"log"
22
"os"
@@ -40,7 +41,13 @@ func Parse(filename string, input []byte) ([]ServerBlock, error) {
41
if err != nil {
42
return nil, err
43
}
43
- p := parser{Dispenser: NewDispenser(tokens)}
44
+ p := parser{
45
+ Dispenser: NewDispenser(tokens),
46
+ importGraph: importGraph{
47
+ nodes: make(map[string]bool),
48
+ edges: make(adjacency),
49
+ },
50
+ }
51
return p.parseAll()
52
}
53
@@ -110,6 +117,7 @@ type parser struct {
117
eof bool // if we encounter a valid EOF in a hard place
118
definedSnippets map[string][]Token
119
nesting int
120
+ importGraph importGraph
121
}
122
123
func (p *parser) parseAll() ([]ServerBlock, error) {
@@ -165,6 +173,15 @@ func (p *parser) begin() error {
173
if err != nil {
174
return err
175
}
176
+ // Just as we need to track which file the token comes from, we need to
177
+ // keep track of which snippets do the tokens come from. This is helpful
178
+ // in tracking import cycles across files/snippets by namespacing them. Without
179
+ // this we end up with false-positives in cycle-detection.
180
+ for k, v := range tokens {
181
+ v.inSnippet = true
182
+ v.snippetName = name
183
+ tokens[k] = v
184
+ }
185
p.definedSnippets[name] = tokens
186
// empty block keys so we don't save this block as a real server.
187
p.block.Keys = nil
@@ -314,10 +331,15 @@ func (p *parser) doImport() error {
331
tokensBefore := p.tokens[:p.cursor-1-len(args)]
332
tokensAfter := p.tokens[p.cursor+1:]
333
var importedTokens []Token
334
+ var nodes []string
335
336
// first check snippets. That is a simple, non-recursive replacement
337
if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
338
importedTokens = p.definedSnippets[importPattern]
339
+ if len(importedTokens) > 0 {
340
+ // just grab the first one
341
+ nodes = append(nodes, fmt.Sprintf("%s:%s", importedTokens[0].File, importedTokens[0].snippetName))
342
+ }
343
} else {
344
// make path relative to the file of the _token_ being processed rather
345
// than current working directory (issue #867) and then use glob to get
@@ -353,7 +375,6 @@ func (p *parser) doImport() error {
375
}
376
377
// collect all the imported tokens
356
-
378
for _, importFile := range matches {
379
newTokens, err := p.doSingleImport(importFile)
380
if err != nil {
@@ -361,6 +382,18 @@ func (p *parser) doImport() error {
382
}
383
importedTokens = append(importedTokens, newTokens...)
384
}
385
+ nodes = matches
386
+ }
387
+
388
+ nodeName := p.File()
389
+ if p.Token().inSnippet {
390
+ nodeName += fmt.Sprintf(":%s", p.Token().snippetName)
391
+ }
392
+ p.importGraph.addNode(nodeName)
393
+ p.importGraph.addNodes(nodes)
394
+ if err := p.importGraph.addEdges(nodeName, nodes); err != nil {
395
+ p.importGraph.removeNodes(nodes)
396
+ return err
397
}
398
399
// copy the tokens so we don't overwrite p.definedSnippets
caddyconfig/caddyfile/parse_test.go
+22
@@ -444,6 +444,28 @@ func TestParseAll(t *testing.T) {
444
445
{`import notfound/*`, false, [][]string{}}, // glob needn't error with no matches
446
{`import notfound/file.conf`, true, [][]string{}}, // but a specific file should
447
+
448
+ // recursive self-import
449
+ {`import testdata/import_recursive0.txt`, true, [][]string{}},
450
+ {`import testdata/import_recursive3.txt
451
+ import testdata/import_recursive1.txt`, true, [][]string{}},
452
+
453
+ // cyclic imports
454
+ {`(A) {
455
+ import A
456
+ }
457
+ :80
458
+ import A
459
+ `, true, [][]string{}},
460
+ {`(A) {
461
+ import B
462
+ }
463
+ (B) {
464
+ import A
465
+ }
466
+ :80
467
+ import A
468
+ `, true, [][]string{}},
469
} {
470
p := testParser(test.input)
471
blocks, err := p.parseAll()
caddyconfig/caddyfile/testdata/import_recursive0.txt
new
+1
@@ -0,0 +1 @@
1
+import import_recursive0.txt
\ No newline at end of file
caddyconfig/caddyfile/testdata/import_recursive1.txt
new
+1
@@ -0,0 +1 @@
1
+import import_recursive2.txt
\ No newline at end of file
caddyconfig/caddyfile/testdata/import_recursive2.txt
new
+1
@@ -0,0 +1 @@
1
+import import_recursive3.txt
\ No newline at end of file
caddyconfig/caddyfile/testdata/import_recursive3.txt
new
+1
@@ -0,0 +1 @@
1
+import import_recursive1.txt
\ No newline at end of file