Skip to content
162 changes: 112 additions & 50 deletions caddyfile/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type parser struct {
block ServerBlock // current server block being parsed
validDirectives []string // a directive must be valid or it's an error
eof bool // if we encounter a valid EOF in a hard place
definedSnippets map[string][]Token
}

func (p *parser) parseAll() ([]ServerBlock, error) {
Expand Down Expand Up @@ -95,6 +96,24 @@ func (p *parser) begin() error {
return nil
}

if ok, name := p.isSnippet(); ok {
if p.definedSnippets == nil {
p.definedSnippets = map[string][]Token{}
}
if _, found := p.definedSnippets[name]; found {
return p.Errf("redeclaration of previously declared snippet %s", name)
}
// consume all tokens til matched close brace
tokens, err := p.snippetTokens()
if err != nil {
return err
}
p.definedSnippets[name] = tokens
// empty block keys so we don't save this block as a real server.
p.block.Keys = nil
return nil
}

return p.blockContents()
}

Expand Down Expand Up @@ -221,70 +240,75 @@ func (p *parser) doImport() error {
if p.NextArg() {
return p.Err("Import takes only one argument (glob pattern or file)")
}
// splice out the import directive and its argument (2 tokens total)
tokensBefore := p.tokens[:p.cursor-1]
tokensAfter := p.tokens[p.cursor+1:]
var importedTokens []Token

// make path relative to Caddyfile rather than current working directory (issue #867)
// and then use glob to get list of matching filenames
absFile, err := filepath.Abs(p.Dispenser.filename)
if err != nil {
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
}

var matches []string
var globPattern string
if !filepath.IsAbs(importPattern) {
globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
// first check snippets. That is a simple, non-recursive replacement
if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
importedTokens = p.definedSnippets[importPattern]
} else {
globPattern = importPattern
}
matches, err = filepath.Glob(globPattern)
// make path relative to Caddyfile rather than current working directory (issue #867)
// and then use glob to get list of matching filenames
absFile, err := filepath.Abs(p.Dispenser.filename)
if err != nil {
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
}

if err != nil {
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
}
if len(matches) == 0 {
if strings.Contains(globPattern, "*") {
log.Printf("[WARNING] No files matching import pattern: %s", importPattern)
var matches []string
var globPattern string
if !filepath.IsAbs(importPattern) {
globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
} else {
return p.Errf("File to import not found: %s", importPattern)
globPattern = importPattern
}
}
matches, err = filepath.Glob(globPattern)

// splice out the import directive and its argument (2 tokens total)
tokensBefore := p.tokens[:p.cursor-1]
tokensAfter := p.tokens[p.cursor+1:]

// collect all the imported tokens
var importedTokens []Token
for _, importFile := range matches {
newTokens, err := p.doSingleImport(importFile)
if err != nil {
return err
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
}
if len(matches) == 0 {
if strings.Contains(globPattern, "*") {
log.Printf("[WARNING] No files matching import pattern: %s", importPattern)
} else {
return p.Errf("File to import not found: %s", importPattern)
}
}

// collect all the imported tokens

var importLine int
for i, token := range newTokens {
if token.Text == "import" {
importLine = token.Line
continue
for _, importFile := range matches {
newTokens, err := p.doSingleImport(importFile)
if err != nil {
return err
}
if token.Line == importLine {
var abs string
if filepath.IsAbs(token.Text) {
abs = token.Text
} else if !filepath.IsAbs(importFile) {
abs = filepath.Join(filepath.Dir(absFile), token.Text)
} else {
abs = filepath.Join(filepath.Dir(importFile), token.Text)

var importLine int
for i, token := range newTokens {
if token.Text == "import" {
importLine = token.Line
continue
}
newTokens[i] = Token{
Text: abs,
Line: token.Line,
File: token.File,
if token.Line == importLine {
var abs string
if filepath.IsAbs(token.Text) {
abs = token.Text
} else if !filepath.IsAbs(importFile) {
abs = filepath.Join(filepath.Dir(absFile), token.Text)
} else {
abs = filepath.Join(filepath.Dir(importFile), token.Text)
}
newTokens[i] = Token{
Text: abs,
Line: token.Line,
File: token.File,
}
}
}
}

importedTokens = append(importedTokens, newTokens...)
importedTokens = append(importedTokens, newTokens...)
}
}

// splice the imported tokens in the place of the import statement
Expand Down Expand Up @@ -433,3 +457,41 @@ type ServerBlock struct {
Keys []string
Tokens map[string][]Token
}

func (p *parser) isSnippet() (bool, string) {
keys := p.block.Keys
// A snippet block is a single key with parens. Nothing else qualifies.
if len(keys) == 1 && strings.HasPrefix(keys[0], "(") && strings.HasSuffix(keys[0], ")") {
return true, strings.TrimSuffix(keys[0][1:], ")")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why TrimSuffix instead of just [1:len(keys[0])-1]?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we explicitly check here that the snippet name inside the parens () does not include a . (dot) this will avoid 1 possible route to confusion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is confusing about it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just think it would be good to avoid

(example.com) {
log example.log "{common}"
}

as a snippet

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Well, I'm curious to see if that will actually be a problem. How about we wait and find out before we add a restriction...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand why it shouldn't be added as a restriction? It needs to be added from the beginning or not at all, otherwise you will have the potential to break installations. The only legitimate use I could see would be if someone wished to create a snippet

 (snippet for all subdomains of example.com) {
      tls off
   }

I wonder how likely that is. I would suggest it is easier to remove the restriction if people feel it gets in the way rather than add it in at a later point.

Copy link
Member

@mholt mholt Nov 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, well, I see what you mean... (I think this change requires that the name in parens is only one word, and I assume your example is just an example in that sense) - what do you think @captncraig ?

}
return false, ""
}

// read and store everything in a block for later replay.
func (p *parser) snippetTokens() ([]Token, error) {
// TODO: disallow imports in snippets for simplicity at import time
// snippet must have curlies.
err := p.openCurlyBrace()
if err != nil {
return nil, err
}
count := 1
tokens := []Token{}
for p.Next() {
if p.Val() == "}" {
count--
if count == 0 {
break
}
}
if p.Val() == "{" {
count++
}
tokens = append(tokens, p.tokens[p.cursor])
}
// make sure we're matched up
if count != 0 {
return nil, p.SyntaxErr("}")
}
return tokens, nil
}
36 changes: 36 additions & 0 deletions caddyfile/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,3 +514,39 @@ func testParser(input string) parser {
p := parser{Dispenser: NewDispenser("Caddyfile", buf)}
return p
}

func TestSnippets(t *testing.T) {
p := testParser(`(common) {
gzip foo
errors stderr

}
http://example.com {
import common
}
`)
blocks, err := p.parseAll()
if err != nil {
t.Fatal(err)
}
for _, b := range blocks {
t.Log(b.Keys)
t.Log(b.Tokens)
}
if len(blocks) != 1 {
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
}
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual {
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
}
if len(blocks[0].Tokens) != 2 {
t.Fatalf("Server block should have tokens from import")
}
if actual, expected := blocks[0].Tokens["gzip"][0].Text, "gzip"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Tokens["errors"][1].Text, "stderr"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}

}