123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498 |
- // Copyright 2015 Light Code Labs, LLC
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- package caddyfile
- import (
- "io"
- "log"
- "os"
- "path/filepath"
- "strings"
- )
- // Parse parses the input just enough to group tokens, in
- // order, by server block. No further parsing is performed.
- // Server blocks are returned in the order in which they appear.
- // Directives that do not appear in validDirectives will cause
- // an error. If you do not want to check for valid directives,
- // pass in nil instead.
- func Parse(filename string, input io.Reader, validDirectives []string) ([]ServerBlock, error) {
- p := parser{Dispenser: NewDispenser(filename, input), validDirectives: validDirectives}
- return p.parseAll()
- }
- // allTokens lexes the entire input, but does not parse it.
- // It returns all the tokens from the input, unstructured
- // and in order.
- func allTokens(input io.Reader) ([]Token, error) {
- l := new(lexer)
- err := l.load(input)
- if err != nil {
- return nil, err
- }
- var tokens []Token
- for l.next() {
- tokens = append(tokens, l.token)
- }
- return tokens, nil
- }
- type parser struct {
- Dispenser
- 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) {
- var blocks []ServerBlock
- for p.Next() {
- err := p.parseOne()
- if err != nil {
- return blocks, err
- }
- if len(p.block.Keys) > 0 {
- blocks = append(blocks, p.block)
- }
- }
- return blocks, nil
- }
- func (p *parser) parseOne() error {
- p.block = ServerBlock{Tokens: make(map[string][]Token)}
- return p.begin()
- }
- 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
- }
- 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()
- }
- func (p *parser) addresses() error {
- var expectingAnother bool
- for {
- tkn := replaceEnvVars(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
- }
- if tkn != "" { // empty token possible if user typed ""
- // 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
- }
- p.block.Keys = append(p.block.Keys, tkn)
- }
- // 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 specified file
- // or globbing pattern. 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 {
- // syntax checks
- if !p.NextArg() {
- return p.ArgErr()
- }
- importPattern := replaceEnvVars(p.Val())
- if importPattern == "" {
- return p.Err("Import requires a non-empty filepath")
- }
- 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
- // first check snippets. That is a simple, non-recursive replacement
- if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
- importedTokens = p.definedSnippets[importPattern]
- } else {
- // 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)
- } else {
- globPattern = importPattern
- }
- matches, err = filepath.Glob(globPattern)
- 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)
- } else {
- return p.Errf("File to import not found: %s", importPattern)
- }
- }
- // collect all the imported tokens
- for _, importFile := range matches {
- newTokens, err := p.doSingleImport(importFile)
- if err != nil {
- return err
- }
- var importLine int
- for i, token := range newTokens {
- if token.Text == "import" {
- importLine = token.Line
- continue
- }
- 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...)
- }
- }
- // splice the imported tokens in the place of the import statement
- // and rewind cursor so Next() will land on first imported token
- p.tokens = append(tokensBefore, append(importedTokens, tokensAfter...)...)
- p.cursor--
- return nil
- }
- // doSingleImport lexes the individual file at importFile and returns
- // its tokens or an error, if any.
- func (p *parser) doSingleImport(importFile string) ([]Token, error) {
- file, err := os.Open(importFile)
- if err != nil {
- return nil, p.Errf("Could not import %s: %v", importFile, err)
- }
- defer file.Close()
- if info, err := file.Stat(); err != nil {
- return nil, p.Errf("Could not import %s: %v", importFile, err)
- } else if info.IsDir() {
- return nil, p.Errf("Could not import %s: is a directory", importFile)
- }
- importedTokens, err := allTokens(file)
- if err != nil {
- return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
- }
- // Tack the file path onto these tokens so errors show the imported file's name
- // (we use full, absolute path to avoid bugs: issue #1892)
- filename, err := filepath.Abs(importFile)
- if err != nil {
- return nil, p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
- }
- for i := 0; i < len(importedTokens); i++ {
- importedTokens[i].File = filename
- }
- return importedTokens, 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
- // TODO: More helpful error message ("did you mean..." or "maybe you need to install its server type")
- if !p.validDirective(dir) {
- 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.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
- 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
- }
- // validDirective returns true if dir is in p.validDirectives.
- func (p *parser) validDirective(dir string) bool {
- if p.validDirectives == nil {
- return true
- }
- for _, d := range p.validDirectives {
- if d == dir {
- return true
- }
- }
- return false
- }
- // replaceEnvVars replaces environment variables that appear in the token
- // and understands both the $UNIX and %WINDOWS% syntaxes.
- func replaceEnvVars(s string) string {
- s = replaceEnvReferences(s, "{%", "%}")
- s = replaceEnvReferences(s, "{$", "}")
- return s
- }
- // replaceEnvReferences performs the actual replacement of env variables
- // in s, given the placeholder start and placeholder end strings.
- func replaceEnvReferences(s, refStart, refEnd string) string {
- index := strings.Index(s, refStart)
- for index != -1 {
- endIndex := strings.Index(s, refEnd)
- if endIndex != -1 {
- ref := s[index : endIndex+len(refEnd)]
- s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1)
- } else {
- return s
- }
- index = strings.Index(s, refStart)
- }
- return s
- }
- // ServerBlock associates any number of keys (usually addresses
- // of some sort) with tokens (grouped by directive name).
- 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:], ")")
- }
- 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
- }
|