Skip to content

Commit 3903642

Browse files
authored
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
1 parent 03b5deb commit 3903642

File tree

8 files changed

+193
-5
lines changed

8 files changed

+193
-5
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ type (
3535

3636
// Token represents a single parsable unit.
3737
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
4143
}
4244
)
4345

caddyconfig/caddyfile/parse.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package caddyfile
1616

1717
import (
1818
"bytes"
19+
"fmt"
1920
"io/ioutil"
2021
"log"
2122
"os"
@@ -40,7 +41,13 @@ func Parse(filename string, input []byte) ([]ServerBlock, error) {
4041
if err != nil {
4142
return nil, err
4243
}
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+
}
4451
return p.parseAll()
4552
}
4653

@@ -110,6 +117,7 @@ type parser struct {
110117
eof bool // if we encounter a valid EOF in a hard place
111118
definedSnippets map[string][]Token
112119
nesting int
120+
importGraph importGraph
113121
}
114122

115123
func (p *parser) parseAll() ([]ServerBlock, error) {
@@ -165,6 +173,15 @@ func (p *parser) begin() error {
165173
if err != nil {
166174
return err
167175
}
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+
}
168185
p.definedSnippets[name] = tokens
169186
// empty block keys so we don't save this block as a real server.
170187
p.block.Keys = nil
@@ -314,10 +331,15 @@ func (p *parser) doImport() error {
314331
tokensBefore := p.tokens[:p.cursor-1-len(args)]
315332
tokensAfter := p.tokens[p.cursor+1:]
316333
var importedTokens []Token
334+
var nodes []string
317335

318336
// first check snippets. That is a simple, non-recursive replacement
319337
if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
320338
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+
}
321343
} else {
322344
// make path relative to the file of the _token_ being processed rather
323345
// than current working directory (issue #867) and then use glob to get
@@ -353,14 +375,25 @@ func (p *parser) doImport() error {
353375
}
354376

355377
// collect all the imported tokens
356-
357378
for _, importFile := range matches {
358379
newTokens, err := p.doSingleImport(importFile)
359380
if err != nil {
360381
return err
361382
}
362383
importedTokens = append(importedTokens, newTokens...)
363384
}
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
364397
}
365398

366399
// copy the tokens so we don't overwrite p.definedSnippets

caddyconfig/caddyfile/parse_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,28 @@ func TestParseAll(t *testing.T) {
444444

445445
{`import notfound/*`, false, [][]string{}}, // glob needn't error with no matches
446446
{`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{}},
447469
} {
448470
p := testParser(test.input)
449471
blocks, err := p.parseAll()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import import_recursive0.txt
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import import_recursive2.txt
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import import_recursive3.txt
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import import_recursive1.txt

0 commit comments

Comments
 (0)