Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 01:00 UTC
| 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 | "bytes" |
| 19 | "fmt" |
| 20 | "io" |
| 21 | "os" |
| 22 | "path/filepath" |
| 23 | "strings" |
| 24 | |
| 25 | "go.uber.org/zap" |
| 26 | |
| 27 | "github.com/caddyserver/caddy/v2" |
| 28 | ) |
| 29 | |
| 30 | // Parse parses the input just enough to group tokens, in |
| 31 | // order, by server block. No further parsing is performed. |
| 32 | // Server blocks are returned in the order in which they appear. |
| 33 | // Directives that do not appear in validDirectives will cause |
| 34 | // an error. If you do not want to check for valid directives, |
| 35 | // pass in nil instead. |
| 36 | // |
| 37 | // Environment variables in {$ENVIRONMENT_VARIABLE} notation |
| 38 | // will be replaced before parsing begins. |
| 39 | func Parse(filename string, input []byte) ([]ServerBlock, error) { |
| 40 | // unfortunately, we must copy the input because parsing must |
| 41 | // remain a read-only operation, but we have to expand environment |
| 42 | // variables before we parse, which changes the underlying array (#4422) |
| 43 | inputCopy := make([]byte, len(input)) |
| 44 | copy(inputCopy, input) |
| 45 | |
| 46 | tokens, err := allTokens(filename, inputCopy) |
| 47 | if err != nil { |
| 48 | return nil, err |
| 49 | } |
| 50 | p := parser{ |
| 51 | Dispenser: NewDispenser(tokens), |
| 52 | importGraph: importGraph{ |
| 53 | nodes: make(map[string]struct{}), |
| 54 | edges: make(adjacency), |
| 55 | }, |
| 56 | } |
| 57 | return p.parseAll() |
| 58 | } |
| 59 | |
| 60 | // allTokens lexes the entire input, but does not parse it. |
| 61 | // It returns all the tokens from the input, unstructured |
| 62 | // and in order. It may mutate input as it expands env vars. |
| 63 | func allTokens(filename string, input []byte) ([]Token, error) { |
| 64 | return Tokenize(replaceEnvVars(input), filename) |
| 65 | } |
| 66 | |
| 67 | // replaceEnvVars replaces all occurrences of environment variables. |
| 68 | // It mutates the underlying array and returns the updated slice. |
| 69 | func replaceEnvVars(input []byte) []byte { |
| 70 | var offset int |
| 71 | for { |
| 72 | begin := bytes.Index(input[offset:], spanOpen) |
| 73 | if begin < 0 { |
| 74 | break |
| 75 | } |
| 76 | begin += offset // make beginning relative to input, not offset |
| 77 | end := bytes.Index(input[begin+len(spanOpen):], spanClose) |
| 78 | if end < 0 { |
| 79 | break |
| 80 | } |
| 81 | end += begin + len(spanOpen) // make end relative to input, not begin |
| 82 | |
| 83 | // get the name; if there is no name, skip it |
| 84 | envString := input[begin+len(spanOpen) : end] |
| 85 | if len(envString) == 0 { |
| 86 | offset = end + len(spanClose) |
| 87 | continue |
| 88 | } |
| 89 | |
| 90 | // split the string into a key and an optional default |
| 91 | envParts := strings.SplitN(string(envString), envVarDefaultDelimiter, 2) |
| 92 | |
| 93 | // do a lookup for the env var, replace with the default if not found |
| 94 | envVarValue, found := os.LookupEnv(envParts[0]) |
| 95 | if !found && len(envParts) == 2 { |
| 96 | envVarValue = envParts[1] |
| 97 | } |
| 98 | |
| 99 | // get the value of the environment variable |
| 100 | // note that this causes one-level deep chaining |
| 101 | envVarBytes := []byte(envVarValue) |
| 102 | |
| 103 | // splice in the value |
| 104 | input = append(input[:begin], |
| 105 | append(envVarBytes, input[end+len(spanClose):]...)...) |
| 106 | |
| 107 | // continue at the end of the replacement |
| 108 | offset = begin + len(envVarBytes) |
| 109 | } |
| 110 | return input |
| 111 | } |
| 112 | |
| 113 | // importAncestor tracks active physical files during formatting import discovery. |
| 114 | // Nodes are immutable and shared by all tokens from one import expansion. |
| 115 | type importAncestor struct { |
| 116 | info os.FileInfo |
| 117 | parent *importAncestor |
| 118 | } |
| 119 | |
| 120 | type parser struct { |
| 121 | *Dispenser |
| 122 | block ServerBlock // current server block being parsed |
| 123 | eof bool // if we encounter a valid EOF in a hard place |
| 124 | definedSnippets map[string][]Token |
| 125 | nesting int |
| 126 | importGraph importGraph |
| 127 | // importObserver, when set, runs after an imported file is opened and read but |
| 128 | // before its tokens are inserted. Returning true skips token insertion. This |
| 129 | // internal hook lets FormatImports observe the parser's actual import execution |
| 130 | // and suppress physical-file cycles without duplicating parser semantics. |
| 131 | importObserver func(string, os.FileInfo, []byte, *importAncestor) (bool, error) |
| 132 | } |
| 133 | |
| 134 | func (p *parser) parseAll() ([]ServerBlock, error) { |
| 135 | var blocks []ServerBlock |
| 136 | |
| 137 | for p.Next() { |
| 138 | err := p.parseOne() |
| 139 | if err != nil { |
| 140 | return blocks, err |
| 141 | } |
| 142 | if len(p.block.Keys) > 0 || len(p.block.Segments) > 0 { |
| 143 | blocks = append(blocks, p.block) |
| 144 | } |
| 145 | if p.nesting > 0 { |
| 146 | return blocks, p.EOFErr() |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | return blocks, nil |
| 151 | } |
| 152 | |
| 153 | func (p *parser) parseOne() error { |
| 154 | p.block = ServerBlock{} |
| 155 | return p.begin() |
| 156 | } |
| 157 | |
| 158 | func (p *parser) begin() error { |
| 159 | if len(p.tokens) == 0 { |
| 160 | return nil |
| 161 | } |
| 162 | |
| 163 | err := p.addresses() |
| 164 | if err != nil { |
| 165 | return err |
| 166 | } |
| 167 | |
| 168 | if p.eof { |
| 169 | // this happens if the Caddyfile consists of only |
| 170 | // a line of addresses and nothing else |
| 171 | return nil |
| 172 | } |
| 173 | |
| 174 | if ok, name := p.isNamedRoute(); ok { |
| 175 | // we just need a dummy leading token to ease parsing later |
| 176 | nameToken := p.Token() |
| 177 | nameToken.Text = name |
| 178 | |
| 179 | // named routes only have one key, the route name |
| 180 | p.block.Keys = []Token{nameToken} |
| 181 | p.block.IsNamedRoute = true |
| 182 | |
| 183 | // get all the tokens from the block, including the braces |
| 184 | tokens, err := p.blockTokens(true) |
| 185 | if err != nil { |
| 186 | return err |
| 187 | } |
| 188 | |
| 189 | // expand any import directives inside the named route block |
| 190 | expandedTokens, err := p.expandImportsInBlock(tokens) |
| 191 | if err != nil { |
| 192 | return err |
| 193 | } |
| 194 | |
| 195 | expandedTokens = append([]Token{nameToken}, expandedTokens...) |
| 196 | p.block.Segments = []Segment{expandedTokens} |
| 197 | return nil |
| 198 | } |
| 199 | |
| 200 | if ok, name := p.isSnippet(); ok { |
| 201 | if p.definedSnippets == nil { |
| 202 | p.definedSnippets = map[string][]Token{} |
| 203 | } |
| 204 | if _, found := p.definedSnippets[name]; found { |
| 205 | return p.Errf("redeclaration of previously declared snippet %s", name) |
| 206 | } |
| 207 | // consume all tokens til matched close brace |
| 208 | tokens, err := p.blockTokens(false) |
| 209 | if err != nil { |
| 210 | return err |
| 211 | } |
| 212 | // Just as we need to track which file the token comes from, we need to |
| 213 | // keep track of which snippet the token comes from. This is helpful |
| 214 | // in tracking import cycles across files/snippets by namespacing them. |
| 215 | // Without this, we end up with false-positives in cycle-detection. |
| 216 | for k, v := range tokens { |
| 217 | v.snippetName = name |
| 218 | tokens[k] = v |
| 219 | } |
| 220 | p.definedSnippets[name] = tokens |
| 221 | // empty block keys so we don't save this block as a real server. |
| 222 | p.block.Keys = nil |
| 223 | return nil |
| 224 | } |
| 225 | |
| 226 | return p.blockContents() |
| 227 | } |
| 228 | |
| 229 | func (p *parser) addresses() error { |
| 230 | var expectingAnother bool |
| 231 | |
| 232 | for { |
| 233 | value := p.Val() |
| 234 | token := p.Token() |
| 235 | |
| 236 | // Reject request matchers if trying to define them globally |
| 237 | if strings.HasPrefix(value, "@") { |
| 238 | return p.Errf("request matchers may not be defined globally, they must be in a site block; found %s", value) |
| 239 | } |
| 240 | |
| 241 | // Special case: import directive replaces tokens during parse-time |
| 242 | if value == "import" && p.isNewLine() { |
| 243 | err := p.doImport(0) |
| 244 | if err != nil { |
| 245 | return err |
| 246 | } |
| 247 | continue |
| 248 | } |
| 249 | |
| 250 | // Open brace definitely indicates end of addresses |
| 251 | if isOpenCurlyBrace(token) { |
| 252 | if expectingAnother { |
| 253 | return p.Errf("Expected another address but had '%s' - check for extra comma", value) |
| 254 | } |
| 255 | // Mark this server block as being defined with braces. |
| 256 | // This is used to provide a better error message when |
| 257 | // the user may have tried to define two server blocks |
| 258 | // without having used braces, which are required in |
| 259 | // that case. |
| 260 | p.block.HasBraces = true |
| 261 | break |
| 262 | } |
| 263 | |
| 264 | // Users commonly forget to place a space between the address and the '{' |
| 265 | if strings.HasSuffix(value, "{") && token.wasQuoted == 0 { |
| 266 | return p.Errf("Site addresses cannot end with a curly brace: '%s' - put a space between the token and the brace", value) |
| 267 | } |
| 268 | |
| 269 | if value != "" { // empty token possible if user typed "" |
| 270 | // Trailing comma indicates another address will follow, which |
| 271 | // may possibly be on the next line |
| 272 | if value[len(value)-1] == ',' { |
| 273 | value = value[:len(value)-1] |
| 274 | expectingAnother = true |
| 275 | } else { |
| 276 | expectingAnother = false // but we may still see another one on this line |
| 277 | } |
| 278 | |
| 279 | // If there's a comma here, it's probably because they didn't use a space |
| 280 | // between their two domains, e.g. "foo.com,bar.com", which would not be |
| 281 | // parsed as two separate site addresses. |
| 282 | if strings.Contains(value, ",") { |
| 283 | return p.Errf("Site addresses cannot contain a comma ',': '%s' - put a space after the comma to separate site addresses", value) |
| 284 | } |
| 285 | |
| 286 | // After the above, a comma surrounded by spaces would result |
| 287 | // in an empty token which we should ignore |
| 288 | if value != "" { |
| 289 | // Add the token as a site address |
| 290 | token.Text = value |
| 291 | p.block.Keys = append(p.block.Keys, token) |
| 292 | } |
| 293 | } |
| 294 | |
| 295 | // Advance token and possibly break out of loop or return error |
| 296 | hasNext := p.Next() |
| 297 | if expectingAnother && !hasNext { |
| 298 | return p.EOFErr() |
| 299 | } |
| 300 | if !hasNext { |
| 301 | p.eof = true |
| 302 | break // EOF |
| 303 | } |
| 304 | if !expectingAnother && p.isNewLine() { |
| 305 | break |
| 306 | } |
| 307 | } |
| 308 | |
| 309 | return nil |
| 310 | } |
| 311 | |
| 312 | func (p *parser) blockContents() error { |
| 313 | errOpenCurlyBrace := p.openCurlyBrace() |
| 314 | if errOpenCurlyBrace != nil { |
| 315 | // single-server configs don't need curly braces |
| 316 | p.cursor-- |
| 317 | } |
| 318 | braced := errOpenCurlyBrace == nil |
| 319 | |
| 320 | closed, err := p.directives(braced) |
| 321 | if err != nil { |
| 322 | return err |
| 323 | } |
| 324 | |
| 325 | // only look for close curly brace if there was an opening |
| 326 | if braced { |
| 327 | if !closed { |
| 328 | // the tokens ran out before the server block was closed; |
| 329 | // the current token may be the closing brace of a nested |
| 330 | // directive block, which does not close this block |
| 331 | return p.EOFErr() |
| 332 | } |
| 333 | err = p.closeCurlyBrace() |
| 334 | if err != nil { |
| 335 | return err |
| 336 | } |
| 337 | } |
| 338 | |
| 339 | return nil |
| 340 | } |
| 341 | |
| 342 | // directives parses through all the lines for directives |
| 343 | // and it expects the next token to be the first |
| 344 | // directive. It goes until EOF or closing curly brace |
| 345 | // which ends the server block. It reports whether it |
| 346 | // stopped at the closing curly brace of a braced server |
| 347 | // block (braced); a closing curly brace is an error if |
| 348 | // the server block was not opened with one. |
| 349 | func (p *parser) directives(braced bool) (bool, error) { |
| 350 | for p.Next() { |
| 351 | // end of server block |
| 352 | if isCloseCurlyBrace(p.Token()) { |
| 353 | if !braced { |
| 354 | return false, p.Err("Unexpected '}' because no matching opening brace") |
| 355 | } |
| 356 | // p.nesting has already been decremented |
| 357 | return true, nil |
| 358 | } |
| 359 | |
| 360 | // special case: import directive replaces tokens during parse-time |
| 361 | if p.Val() == "import" { |
| 362 | err := p.doImport(1) |
| 363 | if err != nil { |
| 364 | return false, err |
| 365 | } |
| 366 | p.cursor-- // cursor is advanced when we continue, so roll back one more |
| 367 | continue |
| 368 | } |
| 369 | |
| 370 | // normal case: parse a directive as a new segment |
| 371 | // (a "segment" is a line which starts with a directive |
| 372 | // and which ends at the end of the line or at the end of |
| 373 | // the block that is opened at the end of the line) |
| 374 | if err := p.directive(); err != nil { |
| 375 | return false, err |
| 376 | } |
| 377 | } |
| 378 | |
| 379 | return false, nil |
| 380 | } |
| 381 | |
| 382 | // doImport swaps out the import directive and its argument |
| 383 | // (a total of 2 tokens) with the tokens in the specified file |
| 384 | // or globbing pattern. When the function returns, the cursor |
| 385 | // is on the token before where the import directive was. In |
| 386 | // other words, call Next() to access the first token that was |
| 387 | // imported. |
| 388 | func (p *parser) doImport(nesting int) error { |
| 389 | // syntax checks |
| 390 | if !p.NextArg() { |
| 391 | return p.ArgErr() |
| 392 | } |
| 393 | importPattern := p.Val() |
| 394 | if importPattern == "" { |
| 395 | return p.Err("Import requires a non-empty filepath") |
| 396 | } |
| 397 | |
| 398 | // grab remaining args as placeholder replacements |
| 399 | args := p.RemainingArgs() |
| 400 | |
| 401 | // set up a replacer for non-variadic args replacement |
| 402 | repl := makeArgsReplacer(args) |
| 403 | |
| 404 | // grab all the tokens (if it exists) from within a block that follows the import |
| 405 | var blockTokens []Token |
| 406 | for currentNesting := p.Nesting(); p.NextBlock(currentNesting); { |
| 407 | blockTokens = append(blockTokens, p.Token()) |
| 408 | } |
| 409 | // initialize with size 1 |
| 410 | blockMapping := make(map[string][]Token, 1) |
| 411 | if len(blockTokens) > 0 { |
| 412 | // use such tokens to create a new dispenser, and then use it to parse each block |
| 413 | bd := NewDispenser(blockTokens) |
| 414 | |
| 415 | // one iteration processes one sub-block inside the import |
| 416 | for bd.Next() { |
| 417 | currentMappingKey := bd.Val() |
| 418 | |
| 419 | if isOpenCurlyBrace(bd.Token()) { |
| 420 | return p.Err("anonymous blocks are not supported") |
| 421 | } |
| 422 | |
| 423 | // load up all arguments (if there even are any) |
| 424 | currentMappingTokens := bd.RemainingArgsAsTokens() |
| 425 | |
| 426 | // load up the entire block |
| 427 | for mappingNesting := bd.Nesting(); bd.NextBlock(mappingNesting); { |
| 428 | currentMappingTokens = append(currentMappingTokens, bd.Token()) |
| 429 | } |
| 430 | |
| 431 | blockMapping[currentMappingKey] = currentMappingTokens |
| 432 | } |
| 433 | } |
| 434 | |
| 435 | // splice out the import directive and its arguments |
| 436 | // (2 tokens, plus the length of args) |
| 437 | tokensBefore := p.tokens[:p.cursor-1-len(args)-len(blockTokens)] |
| 438 | tokensAfter := p.tokens[p.cursor+1:] |
| 439 | var importedTokens []Token |
| 440 | var nodes []string |
| 441 | |
| 442 | // first check snippets. That is a simple, non-recursive replacement |
| 443 | if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil { |
| 444 | importedTokens = p.definedSnippets[importPattern] |
| 445 | if len(importedTokens) > 0 { |
| 446 | // just grab the first one |
| 447 | nodes = append(nodes, fmt.Sprintf("%s:%s", importedTokens[0].File, importedTokens[0].snippetName)) |
| 448 | } |
| 449 | } else { |
| 450 | // make path relative to the file of the _token_ being processed rather |
| 451 | // than current working directory (issue #867) and then use glob to get |
| 452 | // list of matching filenames |
| 453 | matches, globPattern, err := resolveImportGlob(p.Dispenser.File(), importPattern) |
| 454 | if err != nil { |
| 455 | return p.WrapErr(err) |
| 456 | } |
| 457 | if len(matches) == 0 { |
| 458 | if strings.ContainsAny(globPattern, "*?[]") { |
| 459 | caddy.Log().Warn("No files matching import glob pattern", zap.String("pattern", importPattern)) |
| 460 | } else { |
| 461 | // filepath.Glob ignores file system errors (it only |
| 462 | // returns ErrBadPattern), so a file that exists but |
| 463 | // cannot be accessed - e.g. permission denied - looks |
| 464 | // the same as a missing file. Stat the path directly |
| 465 | // so the real error is reported instead (issue #8161). |
| 466 | if _, err := os.Stat(globPattern); err != nil && !os.IsNotExist(err) { |
| 467 | return p.Errf("Failed to import file %s: %v", importPattern, err) |
| 468 | } |
| 469 | return p.Errf("File to import not found: %s", importPattern) |
| 470 | } |
| 471 | } |
| 472 | |
| 473 | // collect all the imported tokens |
| 474 | for _, importFile := range matches { |
| 475 | newTokens, err := p.doSingleImport(importFile) |
| 476 | if err != nil { |
| 477 | return err |
| 478 | } |
| 479 | importedTokens = append(importedTokens, newTokens...) |
| 480 | } |
| 481 | nodes = matches |
| 482 | } |
| 483 | |
| 484 | nodeName := p.File() |
| 485 | if p.Token().snippetName != "" { |
| 486 | nodeName += fmt.Sprintf(":%s", p.Token().snippetName) |
| 487 | } |
| 488 | p.importGraph.addNode(nodeName) |
| 489 | p.importGraph.addNodes(nodes) |
| 490 | if err := p.importGraph.addEdges(nodeName, nodes); err != nil { |
| 491 | p.importGraph.removeNodes(nodes) |
| 492 | return err |
| 493 | } |
| 494 | |
| 495 | // copy the tokens so we don't overwrite p.definedSnippets |
| 496 | tokensCopy := make([]Token, 0, len(importedTokens)) |
| 497 | |
| 498 | var ( |
| 499 | maybeSnippet bool |
| 500 | maybeSnippetId bool |
| 501 | index int |
| 502 | ) |
| 503 | |
| 504 | // run the argument replacer on the tokens |
| 505 | // golang for range slice return a copy of value |
| 506 | // similarly, append also copy value |
| 507 | for i, token := range importedTokens { |
| 508 | if p.importObserver != nil && p.definedSnippets[importPattern] != nil { |
| 509 | // A snippet executes in its caller's ancestry, not the ancestry |
| 510 | // of the file expansion where it was originally declared. |
| 511 | token.importAncestry = p.Token().importAncestry |
| 512 | } |
| 513 | // update the token's imports to refer to import directive filename, line number and snippet name if there is one |
| 514 | if token.snippetName != "" { |
| 515 | token.imports = append(token.imports, fmt.Sprintf("%s:%d (import %s)", p.File(), p.Line(), token.snippetName)) |
| 516 | } else { |
| 517 | token.imports = append(token.imports, fmt.Sprintf("%s:%d (import)", p.File(), p.Line())) |
| 518 | } |
| 519 | |
| 520 | // naive way of determine snippets, as snippets definition can only follow name + block |
| 521 | // format, won't check for nesting correctness or any other error, that's what parser does. |
| 522 | if !maybeSnippet && nesting == 0 { |
| 523 | // first of the line |
| 524 | if i == 0 || isNextOnNewLine(tokensCopy[len(tokensCopy)-1], token) { |
| 525 | index = 0 |
| 526 | } else { |
| 527 | index++ |
| 528 | } |
| 529 | |
| 530 | if index == 0 && len(token.Text) >= 3 && strings.HasPrefix(token.Text, "(") && strings.HasSuffix(token.Text, ")") { |
| 531 | maybeSnippetId = true |
| 532 | } |
| 533 | } |
| 534 | |
| 535 | switch { |
| 536 | case isOpenCurlyBrace(token): |
| 537 | nesting++ |
| 538 | if index == 1 && maybeSnippetId && nesting == 1 { |
| 539 | maybeSnippet = true |
| 540 | maybeSnippetId = false |
| 541 | } |
| 542 | case isCloseCurlyBrace(token): |
| 543 | nesting-- |
| 544 | if nesting == 0 && maybeSnippet { |
| 545 | maybeSnippet = false |
| 546 | } |
| 547 | } |
| 548 | // if it is {block}, we substitute with all tokens in the block |
| 549 | // if it is {blocks.*}, we substitute with the tokens in the mapping for the * |
| 550 | var tokensToAdd []Token |
| 551 | foundBlockDirective := false |
| 552 | switch { |
| 553 | case token.Text == "{block}": |
| 554 | foundBlockDirective = true |
| 555 | tokensToAdd = blockTokens |
| 556 | case strings.HasPrefix(token.Text, "{blocks.") && strings.HasSuffix(token.Text, "}"): |
| 557 | foundBlockDirective = true |
| 558 | // {blocks.foo.bar} will be extracted to key `foo.bar` |
| 559 | blockKey := strings.TrimPrefix(strings.TrimSuffix(token.Text, "}"), "{blocks.") |
| 560 | val, ok := blockMapping[blockKey] |
| 561 | if ok { |
| 562 | tokensToAdd = val |
| 563 | } |
| 564 | } |
| 565 | |
| 566 | if foundBlockDirective { |
| 567 | if maybeSnippet { |
| 568 | tokensCopy = append(tokensCopy, token) |
| 569 | } else { |
| 570 | start := len(tokensCopy) |
| 571 | tokensCopy = append(tokensCopy, tokensToAdd...) |
| 572 | if p.importObserver != nil { |
| 573 | for i := range tokensToAdd { |
| 574 | tokensCopy[start+i].importAncestry = token.importAncestry |
| 575 | } |
| 576 | } |
| 577 | } |
| 578 | continue |
| 579 | } |
| 580 | |
| 581 | if maybeSnippet { |
| 582 | tokensCopy = append(tokensCopy, token) |
| 583 | continue |
| 584 | } |
| 585 | |
| 586 | foundVariadic, startIndex, endIndex := parseVariadic(token, len(args)) |
| 587 | if foundVariadic { |
| 588 | for _, arg := range args[startIndex:endIndex] { |
| 589 | token.Text = arg |
| 590 | tokensCopy = append(tokensCopy, token) |
| 591 | } |
| 592 | } else { |
| 593 | token.Text = repl.ReplaceKnown(token.Text, "") |
| 594 | tokensCopy = append(tokensCopy, token) |
| 595 | } |
| 596 | } |
| 597 | |
| 598 | // splice the imported tokens in the place of the import statement |
| 599 | // and rewind cursor so Next() will land on first imported token |
| 600 | p.tokens = append(tokensBefore, append(tokensCopy, tokensAfter...)...) |
| 601 | p.cursor -= len(args) + len(blockTokens) + 1 |
| 602 | |
| 603 | return nil |
| 604 | } |
| 605 | |
| 606 | // expandImportsInBlock takes a slice of tokens (typically the contents of a |
| 607 | // named route block including its outer curly braces) and expands any import |
| 608 | // directives found at the beginning of a line. The expansion is done by |
| 609 | // creating a temporary parser that shares the same snippets and import graph, |
| 610 | // then looping through the tokens and calling doImport whenever an import |
| 611 | // directive is encountered. All other tokens are left untouched. |
| 612 | func (p *parser) expandImportsInBlock(tokens []Token) ([]Token, error) { |
| 613 | // Create a temporary parser that operates on the provided tokens. |
| 614 | // The Dispenser is initialized with the token slice; snippets and the |
| 615 | // import graph are shared so that imports and cycle detection work |
| 616 | // consistently across the whole Caddyfile. |
| 617 | tempParser := &parser{ |
| 618 | Dispenser: NewDispenser(tokens), |
| 619 | definedSnippets: p.definedSnippets, |
| 620 | importGraph: p.importGraph, |
| 621 | // Carry the observer so callers watching import execution (such as |
| 622 | // FormatImports) also see files imported from inside a named route. |
| 623 | importObserver: p.importObserver, |
| 624 | } |
| 625 | |
| 626 | // Loop through the tokens. We only care about import directives that |
| 627 | // appear at the start of a line (same logic as directives()). |
| 628 | for tempParser.Next() { |
| 629 | if tempParser.Val() == "import" && tempParser.isNewLine() { |
| 630 | if err := tempParser.doImport(1); err != nil { |
| 631 | return nil, err |
| 632 | } |
| 633 | // Roll back the cursor so the next iteration sees the first |
| 634 | // token of the imported content (or the next token after it). |
| 635 | tempParser.cursor-- |
| 636 | } |
| 637 | } |
| 638 | |
| 639 | // The temporary parser's token slice has been modified in place by |
| 640 | // doImport, so we return it directly. |
| 641 | return tempParser.tokens, nil |
| 642 | } |
| 643 | |
| 644 | // doSingleImport lexes the individual file at importFile and returns |
| 645 | // its tokens or an error, if any. |
| 646 | func (p *parser) doSingleImport(importFile string) ([]Token, error) { |
| 647 | file, err := os.Open(importFile) |
| 648 | if err != nil { |
| 649 | return nil, p.Errf("Could not import %s: %v", importFile, err) |
| 650 | } |
| 651 | defer file.Close() |
| 652 | |
| 653 | info, err := file.Stat() |
| 654 | if err != nil { |
| 655 | return nil, p.Errf("Could not import %s: %v", importFile, err) |
| 656 | } |
| 657 | if info.IsDir() { |
| 658 | return nil, p.Errf("Could not import %s: is a directory", importFile) |
| 659 | } |
| 660 | |
| 661 | input, err := io.ReadAll(file) |
| 662 | if err != nil { |
| 663 | return nil, p.Errf("Could not read imported file %s: %v", importFile, err) |
| 664 | } |
| 665 | // Notify only after a successful read so observers receive bytes and metadata |
| 666 | // from the same open descriptor. A skipped cyclic import contributes no |
| 667 | // tokens at this occurrence. |
| 668 | if p.importObserver != nil { |
| 669 | skip, err := p.importObserver(importFile, info, input, p.Token().importAncestry) |
| 670 | if err != nil { |
| 671 | return nil, p.Errf("Could not import %s: %v", importFile, err) |
| 672 | } |
| 673 | if skip { |
| 674 | return nil, nil |
| 675 | } |
| 676 | } |
| 677 | |
| 678 | // only warning in case of empty files |
| 679 | if len(input) == 0 || len(strings.TrimSpace(string(input))) == 0 { |
| 680 | caddy.Log().Warn("Import file is empty", zap.String("file", importFile)) |
| 681 | return []Token{}, nil |
| 682 | } |
| 683 | |
| 684 | importedTokens, err := allTokens(importFile, input) |
| 685 | if err != nil { |
| 686 | return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err) |
| 687 | } |
| 688 | |
| 689 | // Tack the file path onto these tokens so errors show the imported file's name |
| 690 | // (we use full, absolute path to avoid bugs: issue #1892) |
| 691 | filename, err := caddy.FastAbs(importFile) |
| 692 | if err != nil { |
| 693 | return nil, p.Errf("Failed to get absolute path of file: %s: %v", importFile, err) |
| 694 | } |
| 695 | var ancestry *importAncestor |
| 696 | if p.importObserver != nil { |
| 697 | ancestry = &importAncestor{info: info, parent: p.Token().importAncestry} |
| 698 | } |
| 699 | for i := range importedTokens { |
| 700 | importedTokens[i].File = filename |
| 701 | importedTokens[i].importAncestry = ancestry |
| 702 | } |
| 703 | |
| 704 | return importedTokens, nil |
| 705 | } |
| 706 | |
| 707 | // directive collects tokens until the directive's scope |
| 708 | // closes (either end of line or end of curly brace block). |
| 709 | // It expects the currently-loaded token to be a directive |
| 710 | // (or } that ends a server block). The collected tokens |
| 711 | // are loaded into the current server block for later use |
| 712 | // by directive setup functions. |
| 713 | func (p *parser) directive() error { |
| 714 | // a segment is a list of tokens associated with this directive |
| 715 | var segment Segment |
| 716 | |
| 717 | // the directive itself is appended as a relevant token |
| 718 | segment = append(segment, p.Token()) |
| 719 | |
| 720 | for p.Next() { |
| 721 | if isOpenCurlyBrace(p.Token()) { |
| 722 | p.nesting++ |
| 723 | if !p.isNextOnNewLine() { |
| 724 | return p.Err("Unexpected next token after '{' on same line") |
| 725 | } |
| 726 | if p.isNewLine() { |
| 727 | return p.Err("Unexpected '{' on a new line; did you mean to place the '{' on the previous line?") |
| 728 | } |
| 729 | } else if p.Val() == "{}" && p.Token().wasQuoted == 0 { |
| 730 | if p.isNextOnNewLine() { |
| 731 | return p.Err("Unexpected '{}' at end of line") |
| 732 | } |
| 733 | } else if p.isNewLine() && p.nesting == 0 { |
| 734 | p.cursor-- // read too far |
| 735 | break |
| 736 | } else if isCloseCurlyBrace(p.Token()) && p.nesting > 0 { |
| 737 | p.nesting-- |
| 738 | } else if isCloseCurlyBrace(p.Token()) && p.nesting == 0 { |
| 739 | return p.Err("Unexpected '}' because no matching opening brace") |
| 740 | } else if p.Val() == "import" && p.isNewLine() { |
| 741 | if err := p.doImport(1); err != nil { |
| 742 | return err |
| 743 | } |
| 744 | p.cursor-- // cursor is advanced when we continue, so roll back one more |
| 745 | continue |
| 746 | } |
| 747 | |
| 748 | segment = append(segment, p.Token()) |
| 749 | } |
| 750 | |
| 751 | p.block.Segments = append(p.block.Segments, segment) |
| 752 | |
| 753 | if p.nesting > 0 { |
| 754 | return p.EOFErr() |
| 755 | } |
| 756 | |
| 757 | return nil |
| 758 | } |
| 759 | |
| 760 | // openCurlyBrace expects the current token to be an |
| 761 | // opening curly brace. This acts like an assertion |
| 762 | // because it returns an error if the token is not |
| 763 | // an opening curly brace. It does NOT advance the token. |
| 764 | func (p *parser) openCurlyBrace() error { |
| 765 | if !isOpenCurlyBrace(p.Token()) { |
| 766 | if p.valLooksLikeGlobalOptionsAfterImportedSnippets() { |
| 767 | return p.Err("global options block must appear before import directives; move the global options block to the top of the Caddyfile") |
| 768 | } |
| 769 | return p.SyntaxErr("{") |
| 770 | } |
| 771 | return nil |
| 772 | } |
| 773 | |
| 774 | func (p *parser) valLooksLikeGlobalOptionsAfterImportedSnippets() bool { |
| 775 | if p.Val() != "import" || len(p.block.Keys) == 0 { |
| 776 | return false |
| 777 | } |
| 778 | |
| 779 | for _, key := range p.block.Keys { |
| 780 | if !strings.HasPrefix(key.Text, "(") || !strings.HasSuffix(key.Text, ")") { |
| 781 | return false |
| 782 | } |
| 783 | } |
| 784 | |
| 785 | return true |
| 786 | } |
| 787 | |
| 788 | // closeCurlyBrace expects the current token to be |
| 789 | // a closing curly brace. This acts like an assertion |
| 790 | // because it returns an error if the token is not |
| 791 | // a closing curly brace. It does NOT advance the token. |
| 792 | func (p *parser) closeCurlyBrace() error { |
| 793 | if !isCloseCurlyBrace(p.Token()) { |
| 794 | return p.SyntaxErr("}") |
| 795 | } |
| 796 | return nil |
| 797 | } |
| 798 | |
| 799 | func (p *parser) isNamedRoute() (bool, string) { |
| 800 | keys := p.block.Keys |
| 801 | // A named route block is a single key with parens, prefixed with &. |
| 802 | if len(keys) == 1 && strings.HasPrefix(keys[0].Text, "&(") && strings.HasSuffix(keys[0].Text, ")") { |
| 803 | return true, strings.TrimSuffix(keys[0].Text[2:], ")") |
| 804 | } |
| 805 | return false, "" |
| 806 | } |
| 807 | |
| 808 | func (p *parser) isSnippet() (bool, string) { |
| 809 | keys := p.block.Keys |
| 810 | // A snippet block is a single key with parens. Nothing else qualifies. |
| 811 | if len(keys) == 1 && strings.HasPrefix(keys[0].Text, "(") && strings.HasSuffix(keys[0].Text, ")") { |
| 812 | return true, strings.TrimSuffix(keys[0].Text[1:], ")") |
| 813 | } |
| 814 | return false, "" |
| 815 | } |
| 816 | |
| 817 | // read and store everything in a block for later replay. |
| 818 | func (p *parser) blockTokens(retainCurlies bool) ([]Token, error) { |
| 819 | // block must have curlies. |
| 820 | err := p.openCurlyBrace() |
| 821 | if err != nil { |
| 822 | return nil, err |
| 823 | } |
| 824 | nesting := 1 // count our own nesting |
| 825 | tokens := []Token{} |
| 826 | if retainCurlies { |
| 827 | tokens = append(tokens, p.Token()) |
| 828 | } |
| 829 | for p.Next() { |
| 830 | if isCloseCurlyBrace(p.Token()) { |
| 831 | nesting-- |
| 832 | if nesting == 0 { |
| 833 | if retainCurlies { |
| 834 | tokens = append(tokens, p.Token()) |
| 835 | } |
| 836 | break |
| 837 | } |
| 838 | } |
| 839 | if isOpenCurlyBrace(p.Token()) { |
| 840 | nesting++ |
| 841 | } |
| 842 | tokens = append(tokens, p.tokens[p.cursor]) |
| 843 | } |
| 844 | // make sure we're matched up |
| 845 | if nesting != 0 { |
| 846 | return nil, p.SyntaxErr("}") |
| 847 | } |
| 848 | return tokens, nil |
| 849 | } |
| 850 | |
| 851 | // ServerBlock associates any number of keys from the |
| 852 | // head of the server block with tokens, which are |
| 853 | // grouped by segments. |
| 854 | type ServerBlock struct { |
| 855 | HasBraces bool |
| 856 | Keys []Token |
| 857 | Segments []Segment |
| 858 | IsNamedRoute bool |
| 859 | } |
| 860 | |
| 861 | func (sb ServerBlock) GetKeysText() []string { |
| 862 | res := make([]string, 0, len(sb.Keys)) |
| 863 | for _, k := range sb.Keys { |
| 864 | res = append(res, k.Text) |
| 865 | } |
| 866 | return res |
| 867 | } |
| 868 | |
| 869 | // DispenseDirective returns a dispenser that contains |
| 870 | // all the tokens in the server block. |
| 871 | func (sb ServerBlock) DispenseDirective(dir string) *Dispenser { |
| 872 | var tokens []Token |
| 873 | for _, seg := range sb.Segments { |
| 874 | if len(seg) > 0 && seg[0].Text == dir { |
| 875 | tokens = append(tokens, seg...) |
| 876 | } |
| 877 | } |
| 878 | return NewDispenser(tokens) |
| 879 | } |
| 880 | |
| 881 | // Segment is a list of tokens which begins with a directive |
| 882 | // and ends at the end of the directive (either at the end of |
| 883 | // the line, or at the end of a block it opens). |
| 884 | type Segment []Token |
| 885 | |
| 886 | // Directive returns the directive name for the segment. |
| 887 | // The directive name is the text of the first token. |
| 888 | func (s Segment) Directive() string { |
| 889 | if len(s) > 0 { |
| 890 | return s[0].Text |
| 891 | } |
| 892 | return "" |
| 893 | } |
| 894 | |
| 895 | // resolveImportGlob resolves importPattern relative to importerFile into a |
| 896 | // list of matching file paths. It handles making the pattern absolute (issue |
| 897 | // #867), enforces the single-wildcard limit (issue #2096), runs filepath.Glob, |
| 898 | // and skips dot-files for glob matches (issue #5295). It returns the matched |
| 899 | // paths, the resolved glob pattern (so callers can distinguish glob vs literal |
| 900 | // for warning/error purposes), and any hard error. Empty-match handling (the |
| 901 | // Warn vs "not found" decision) is left to the caller. |
| 902 | func resolveImportGlob(importerFile, importPattern string) (matches []string, globPattern string, err error) { |
| 903 | absFile, err := caddy.FastAbs(importerFile) |
| 904 | if err != nil { |
| 905 | return nil, "", fmt.Errorf("failed to get absolute path of file: %s: %v", importerFile, err) |
| 906 | } |
| 907 | |
| 908 | if !filepath.IsAbs(importPattern) { |
| 909 | globPattern = filepath.Join(filepath.Dir(absFile), importPattern) |
| 910 | } else { |
| 911 | globPattern = importPattern |
| 912 | } |
| 913 | |
| 914 | if strings.Count(globPattern, "*") > 1 || strings.Count(globPattern, "?") > 1 || |
| 915 | (strings.Contains(globPattern, "[") && strings.Contains(globPattern, "]")) { |
| 916 | // See issue #2096 - a pattern with many glob expansions can hang for too long |
| 917 | return nil, globPattern, fmt.Errorf("glob pattern may only contain one wildcard (*), but has others: %s", globPattern) |
| 918 | } |
| 919 | |
| 920 | matches, err = filepath.Glob(globPattern) |
| 921 | if err != nil { |
| 922 | return nil, globPattern, fmt.Errorf("failed to use import pattern %s: %v", importPattern, err) |
| 923 | } |
| 924 | |
| 925 | // See issue #5295 - skip files that start with a . when the last path |
| 926 | // segment of the glob pattern starts with *. |
| 927 | sep := string(filepath.Separator) |
| 928 | segGlobPattern := strings.Split(globPattern, sep) |
| 929 | if strings.HasPrefix(segGlobPattern[len(segGlobPattern)-1], "*") { |
| 930 | var filtered []string |
| 931 | for _, m := range matches { |
| 932 | seg := strings.Split(m, sep) |
| 933 | if !strings.HasPrefix(seg[len(seg)-1], ".") { |
| 934 | filtered = append(filtered, m) |
| 935 | } |
| 936 | } |
| 937 | matches = filtered |
| 938 | } |
| 939 | |
| 940 | return matches, globPattern, nil |
| 941 | } |
| 942 | |
| 943 | // spanOpen and spanClose are used to bound spans that |
| 944 | // contain the name of an environment variable. |
| 945 | var ( |
| 946 | spanOpen, spanClose = []byte{'{', '$'}, []byte{'}'} |
| 947 | envVarDefaultDelimiter = ":" |
| 948 | ) |