@marcel91200 / caddy Mirror · read-only / commits / 3903642a
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 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