mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-23 22:27:38 -05:00
0ac8bf58ea
Even if defined for multiple hosts. Startup or shutdown callbacks registered by any directive (startup, shutdown, markdown, git, log, etc.) will only run as many times as it appears in the Caddyfile, not repeated for each host that shares that server block. Fixing this involved refactoring three packages (yeesh) and we need to restore some tests that are no longer valid (that used to verify splitting a multiServerBlock into multiple serverBlocks).
311 lines
6.9 KiB
Go
311 lines
6.9 KiB
Go
package parse
|
|
|
|
import (
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type parser struct {
|
|
Dispenser
|
|
block ServerBlock // current server block being parsed
|
|
eof bool // if we encounter a valid EOF in a hard place
|
|
}
|
|
|
|
func (p *parser) parseAll() ([]ServerBlock, error) {
|
|
var blocks []ServerBlock
|
|
|
|
for p.Next() {
|
|
err := p.parseOne()
|
|
if err != nil {
|
|
return blocks, err
|
|
}
|
|
blocks = append(blocks, p.block)
|
|
}
|
|
|
|
return blocks, nil
|
|
}
|
|
|
|
func (p *parser) parseOne() error {
|
|
p.block = ServerBlock{Tokens: make(map[string][]token)}
|
|
|
|
err := p.begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *parser) begin() error {
|
|
if len(p.tokens) == 0 {
|
|
return nil
|
|
}
|
|
|
|
err := p.addresses()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if p.eof {
|
|
// this happens if the Caddyfile consists of only
|
|
// a line of addresses and nothing else
|
|
return nil
|
|
}
|
|
|
|
err = p.blockContents()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *parser) addresses() error {
|
|
var expectingAnother bool
|
|
|
|
for {
|
|
tkn := p.Val()
|
|
|
|
// special case: import directive replaces tokens during parse-time
|
|
if tkn == "import" && p.isNewLine() {
|
|
err := p.doImport()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Open brace definitely indicates end of addresses
|
|
if tkn == "{" {
|
|
if expectingAnother {
|
|
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
|
|
}
|
|
break
|
|
}
|
|
|
|
// Trailing comma indicates another address will follow, which
|
|
// may possibly be on the next line
|
|
if tkn[len(tkn)-1] == ',' {
|
|
tkn = tkn[:len(tkn)-1]
|
|
expectingAnother = true
|
|
} else {
|
|
expectingAnother = false // but we may still see another one on this line
|
|
}
|
|
|
|
// Parse and save this address
|
|
host, port, err := standardAddress(tkn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.block.Addresses = append(p.block.Addresses, Address{host, port})
|
|
|
|
// Advance token and possibly break out of loop or return error
|
|
hasNext := p.Next()
|
|
if expectingAnother && !hasNext {
|
|
return p.EofErr()
|
|
}
|
|
if !hasNext {
|
|
p.eof = true
|
|
break // EOF
|
|
}
|
|
if !expectingAnother && p.isNewLine() {
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *parser) blockContents() error {
|
|
errOpenCurlyBrace := p.openCurlyBrace()
|
|
if errOpenCurlyBrace != nil {
|
|
// single-server configs don't need curly braces
|
|
p.cursor--
|
|
}
|
|
|
|
err := p.directives()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Only look for close curly brace if there was an opening
|
|
if errOpenCurlyBrace == nil {
|
|
err = p.closeCurlyBrace()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// directives parses through all the lines for directives
|
|
// and it expects the next token to be the first
|
|
// directive. It goes until EOF or closing curly brace
|
|
// which ends the server block.
|
|
func (p *parser) directives() error {
|
|
for p.Next() {
|
|
// end of server block
|
|
if p.Val() == "}" {
|
|
break
|
|
}
|
|
|
|
// special case: import directive replaces tokens during parse-time
|
|
if p.Val() == "import" {
|
|
err := p.doImport()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
|
continue
|
|
}
|
|
|
|
// normal case: parse a directive on this line
|
|
if err := p.directive(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// doImport swaps out the import directive and its argument
|
|
// (a total of 2 tokens) with the tokens in the file specified.
|
|
// When the function returns, the cursor is on the token before
|
|
// where the import directive was. In other words, call Next()
|
|
// to access the first token that was imported.
|
|
func (p *parser) doImport() error {
|
|
if !p.NextArg() {
|
|
return p.ArgErr()
|
|
}
|
|
importFile := p.Val()
|
|
if p.NextArg() {
|
|
return p.Err("Import allows only one file to import")
|
|
}
|
|
|
|
file, err := os.Open(importFile)
|
|
if err != nil {
|
|
return p.Errf("Could not import %s - %v", importFile, err)
|
|
}
|
|
defer file.Close()
|
|
importedTokens := allTokens(file)
|
|
|
|
// Tack the filename onto these tokens so any errors show the imported file's name
|
|
for i := 0; i < len(importedTokens); i++ {
|
|
importedTokens[i].file = filepath.Base(importFile)
|
|
}
|
|
|
|
// Splice out the import directive and its argument (2 tokens total)
|
|
// and insert the imported tokens in their place.
|
|
tokensBefore := p.tokens[:p.cursor-1]
|
|
tokensAfter := p.tokens[p.cursor+1:]
|
|
p.tokens = append(tokensBefore, append(importedTokens, tokensAfter...)...)
|
|
p.cursor-- // cursor was advanced one position to read the filename; rewind it
|
|
|
|
return nil
|
|
}
|
|
|
|
// directive collects tokens until the directive's scope
|
|
// closes (either end of line or end of curly brace block).
|
|
// It expects the currently-loaded token to be a directive
|
|
// (or } that ends a server block). The collected tokens
|
|
// are loaded into the current server block for later use
|
|
// by directive setup functions.
|
|
func (p *parser) directive() error {
|
|
dir := p.Val()
|
|
nesting := 0
|
|
|
|
if _, ok := ValidDirectives[dir]; !ok {
|
|
return p.Errf("Unknown directive '%s'", dir)
|
|
}
|
|
|
|
// The directive itself is appended as a relevant token
|
|
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
|
|
|
for p.Next() {
|
|
if p.Val() == "{" {
|
|
nesting++
|
|
} else if p.isNewLine() && nesting == 0 {
|
|
p.cursor-- // read too far
|
|
break
|
|
} else if p.Val() == "}" && nesting > 0 {
|
|
nesting--
|
|
} else if p.Val() == "}" && nesting == 0 {
|
|
return p.Err("Unexpected '}' because no matching opening brace")
|
|
}
|
|
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
|
}
|
|
|
|
if nesting > 0 {
|
|
return p.EofErr()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// openCurlyBrace expects the current token to be an
|
|
// opening curly brace. This acts like an assertion
|
|
// because it returns an error if the token is not
|
|
// a opening curly brace. It does NOT advance the token.
|
|
func (p *parser) openCurlyBrace() error {
|
|
if p.Val() != "{" {
|
|
return p.SyntaxErr("{")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// closeCurlyBrace expects the current token to be
|
|
// a closing curly brace. This acts like an assertion
|
|
// because it returns an error if the token is not
|
|
// a closing curly brace. It does NOT advance the token.
|
|
func (p *parser) closeCurlyBrace() error {
|
|
if p.Val() != "}" {
|
|
return p.SyntaxErr("}")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// standardAddress turns the accepted host and port patterns
|
|
// into a format accepted by net.Dial.
|
|
func standardAddress(str string) (host, port string, err error) {
|
|
var schemePort, splitPort string
|
|
|
|
if strings.HasPrefix(str, "https://") {
|
|
schemePort = "https"
|
|
str = str[8:]
|
|
} else if strings.HasPrefix(str, "http://") {
|
|
schemePort = "http"
|
|
str = str[7:]
|
|
}
|
|
|
|
host, splitPort, err = net.SplitHostPort(str)
|
|
if err != nil {
|
|
host, splitPort, err = net.SplitHostPort(str + ":") // tack on empty port
|
|
}
|
|
if err != nil {
|
|
// ¯\_(ツ)_/¯
|
|
host = str
|
|
}
|
|
|
|
if splitPort != "" {
|
|
port = splitPort
|
|
} else {
|
|
port = schemePort
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
type (
|
|
// ServerBlock associates tokens with a list of addresses
|
|
// and groups tokens by directive name.
|
|
ServerBlock struct {
|
|
Addresses []Address
|
|
Tokens map[string][]token
|
|
}
|
|
|
|
// Address represents a host and port.
|
|
Address struct {
|
|
Host, Port string
|
|
}
|
|
)
|