@marcel91200 / caddy Mirror · read-only
Fork 0

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

master
go 793 lines 24.1 KB
Raw
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 fileserver
16
17 import (
18 "fmt"
19 "io/fs"
20 "net/http"
21 "os"
22 "path/filepath"
23 "runtime"
24 "strconv"
25 "strings"
26
27 "cel.dev/cel-go/cel"
28 "cel.dev/cel-go/common"
29 "cel.dev/cel-go/common/ast"
30 "cel.dev/cel-go/common/operators"
31 "cel.dev/cel-go/common/types"
32 "cel.dev/cel-go/common/types/ref"
33 "cel.dev/cel-go/parser"
34 "go.uber.org/zap"
35 "go.uber.org/zap/zapcore"
36
37 "github.com/caddyserver/caddy/v2"
38 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
39 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
40 )
41
42 func init() {
43 caddy.RegisterModule(MatchFile{})
44 }
45
46 // MatchFile is an HTTP request matcher that can match
47 // requests based upon file existence.
48 //
49 // Upon matching, three new placeholders will be made
50 // available:
51 //
52 // - `{http.matchers.file.relative}` The root-relative
53 // path of the file. This is often useful when rewriting
54 // requests.
55 // - `{http.matchers.file.absolute}` The absolute path
56 // of the matched file.
57 // - `{http.matchers.file.type}` Set to "directory" if
58 // the matched file is a directory, "file" otherwise.
59 // - `{http.matchers.file.remainder}` Set to the remainder
60 // of the path if the path was split by `split_path`.
61 //
62 // Even though file matching may depend on the OS path
63 // separator, the placeholder values always use /.
64 type MatchFile struct {
65 // The file system implementation to use. By default, the
66 // local disk file system will be used.
67 FileSystem string `json:"fs,omitempty"`
68
69 // The root directory, used for creating absolute
70 // file paths, and required when working with
71 // relative paths; if not specified, `{http.vars.root}`
72 // will be used, if set; otherwise, the current
73 // directory is assumed. Accepts placeholders.
74 Root string `json:"root,omitempty"`
75
76 // The list of files to try. Each path here is
77 // considered related to Root. If nil, the request
78 // URL's path will be assumed. Files and
79 // directories are treated distinctly, so to match
80 // a directory, the filepath MUST end in a forward
81 // slash `/`. To match a regular file, there must
82 // be no trailing slash. Accepts placeholders. If
83 // the policy is "first_exist", then an error may
84 // be triggered as a fallback by configuring "="
85 // followed by a status code number,
86 // for example "=404".
87 TryFiles []string `json:"try_files,omitempty"`
88
89 // How to choose a file in TryFiles. Can be:
90 //
91 // - first_exist
92 // - first_exist_fallback
93 // - smallest_size
94 // - largest_size
95 // - most_recently_modified
96 //
97 // Default is first_exist.
98 TryPolicy string `json:"try_policy,omitempty"`
99
100 // A list of delimiters to use to split the path in two
101 // when trying files. If empty, no splitting will
102 // occur, and the path will be tried as-is. For each
103 // split value, the left-hand side of the split,
104 // including the split value, will be the path tried.
105 // For example, the path `/remote.php/dav/` using the
106 // split value `.php` would try the file `/remote.php`.
107 // Each delimiter must appear at the end of a URI path
108 // component in order to be used as a split delimiter.
109 SplitPath []string `json:"split_path,omitempty"`
110
111 fsmap caddy.FileSystems
112
113 logger *zap.Logger
114 }
115
116 // CaddyModule returns the Caddy module information.
117 func (MatchFile) CaddyModule() caddy.ModuleInfo {
118 return caddy.ModuleInfo{
119 ID: "http.matchers.file",
120 New: func() caddy.Module { return new(MatchFile) },
121 }
122 }
123
124 // UnmarshalCaddyfile sets up the matcher from Caddyfile tokens. Syntax:
125 //
126 // file <files...> {
127 // root <path>
128 // try_files <files...>
129 // try_policy first_exist|smallest_size|largest_size|most_recently_modified
130 // }
131 func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
132 // iterate to merge multiple matchers into one
133 for d.Next() {
134 m.TryFiles = append(m.TryFiles, d.RemainingArgs()...)
135 for d.NextBlock(0) {
136 switch d.Val() {
137 case "root":
138 if !d.NextArg() {
139 return d.ArgErr()
140 }
141 m.Root = d.Val()
142 case "try_files":
143 m.TryFiles = append(m.TryFiles, d.RemainingArgs()...)
144 if len(m.TryFiles) == 0 {
145 return d.ArgErr()
146 }
147 case "try_policy":
148 if !d.NextArg() {
149 return d.ArgErr()
150 }
151 m.TryPolicy = d.Val()
152 case "split_path":
153 m.SplitPath = d.RemainingArgs()
154 if len(m.SplitPath) == 0 {
155 return d.ArgErr()
156 }
157 default:
158 return d.Errf("unrecognized subdirective: %s", d.Val())
159 }
160 }
161 }
162 return nil
163 }
164
165 // CELLibrary produces options that expose this matcher for use in CEL
166 // expression matchers.
167 //
168 // Example:
169 //
170 // expression file()
171 // expression file({http.request.uri.path}, '/index.php')
172 // expression file({'root': '/srv', 'try_files': [{http.request.uri.path}, '/index.php'], 'try_policy': 'first_exist', 'split_path': ['.php']})
173 func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) {
174 requestType := cel.ObjectType("http.Request")
175
176 matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcherWithError, error) {
177 values, err := caddyhttp.CELValueToMapStrList(data)
178 if err != nil {
179 return nil, err
180 }
181
182 var root string
183 if len(values["root"]) > 0 {
184 root = values["root"][0]
185 }
186
187 var fsName string
188 if len(values["fs"]) > 0 {
189 fsName = values["fs"][0]
190 }
191
192 var try_policy string
193 if len(values["try_policy"]) > 0 {
194 try_policy = values["try_policy"][0]
195 }
196
197 m := MatchFile{
198 Root: root,
199 TryFiles: values["try_files"],
200 TryPolicy: try_policy,
201 SplitPath: values["split_path"],
202 FileSystem: fsName,
203 }
204
205 err = m.Provision(ctx)
206 return m, err
207 }
208
209 envOptions := []cel.EnvOption{
210 cel.Macros(parser.NewGlobalVarArgMacro("file", celFileMatcherMacroExpander())),
211 cel.Function("file", cel.Overload("file_request_map", []*cel.Type{requestType, caddyhttp.CELTypeJSON}, cel.BoolType)),
212 cel.Function("file_request_map",
213 cel.Overload("file_request_map", []*cel.Type{requestType, caddyhttp.CELTypeJSON}, cel.BoolType),
214 cel.SingletonBinaryBinding(caddyhttp.CELMatcherRuntimeFunction("file_request_map", matcherFactory))),
215 }
216
217 programOptions := []cel.ProgramOption{
218 cel.CustomDecorator(caddyhttp.CELMatcherDecorator("file_request_map", matcherFactory)),
219 }
220
221 return caddyhttp.NewMatcherCELLibrary(envOptions, programOptions), nil
222 }
223
224 func celFileMatcherMacroExpander() parser.MacroExpander {
225 return func(eh parser.ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) {
226 if len(args) == 0 {
227 return eh.NewCall("file",
228 eh.NewIdent(caddyhttp.CELRequestVarName),
229 eh.NewMap(),
230 ), nil
231 }
232 if len(args) == 1 {
233 arg := args[0]
234 if isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg) {
235 return eh.NewCall("file",
236 eh.NewIdent(caddyhttp.CELRequestVarName),
237 eh.NewMap(eh.NewMapEntry(
238 eh.NewLiteral(types.String("try_files")),
239 eh.NewList(arg),
240 false,
241 )),
242 ), nil
243 }
244 if isCELTryFilesLiteral(arg) {
245 return eh.NewCall("file", eh.NewIdent(caddyhttp.CELRequestVarName), arg), nil
246 }
247 return nil, &common.Error{
248 Location: eh.OffsetLocation(arg.ID()),
249 Message: "matcher requires either a map or string literal argument",
250 }
251 }
252
253 for _, arg := range args {
254 if !isCELStringLiteral(arg) && !isCELCaddyPlaceholderCall(arg) {
255 return nil, &common.Error{
256 Location: eh.OffsetLocation(arg.ID()),
257 Message: "matcher only supports repeated string literal arguments",
258 }
259 }
260 }
261 return eh.NewCall("file",
262 eh.NewIdent(caddyhttp.CELRequestVarName),
263 eh.NewMap(eh.NewMapEntry(
264 eh.NewLiteral(types.String("try_files")),
265 eh.NewList(args...),
266 false,
267 )),
268 ), nil
269 }
270 }
271
272 // Provision sets up m's defaults.
273 func (m *MatchFile) Provision(ctx caddy.Context) error {
274 m.logger = ctx.Logger()
275
276 m.fsmap = ctx.FileSystems()
277
278 if m.Root == "" {
279 m.Root = "{http.vars.root}"
280 }
281
282 if m.FileSystem == "" {
283 m.FileSystem = "{http.vars.fs}"
284 }
285
286 // if list of files to try was omitted entirely, assume URL path
287 // (use placeholder instead of r.URL.Path; see issue #4146)
288 if m.TryFiles == nil {
289 m.TryFiles = []string{"{http.request.uri.path}"}
290 }
291 return nil
292 }
293
294 // Validate ensures m has a valid configuration.
295 func (m MatchFile) Validate() error {
296 switch m.TryPolicy {
297 case "",
298 tryPolicyFirstExist,
299 tryPolicyFirstExistFallback,
300 tryPolicyLargestSize,
301 tryPolicySmallestSize,
302 tryPolicyMostRecentlyMod:
303 default:
304 return fmt.Errorf("unknown try policy %s", m.TryPolicy)
305 }
306 return nil
307 }
308
309 // Match returns true if r matches m. Returns true
310 // if a file was matched. If so, four placeholders
311 // will be available:
312 // - http.matchers.file.relative: Path to file relative to site root
313 // - http.matchers.file.absolute: Path to file including site root
314 // - http.matchers.file.type: file or directory
315 // - http.matchers.file.remainder: Portion remaining after splitting file path (if configured)
316 func (m MatchFile) Match(r *http.Request) bool {
317 match, err := m.selectFile(r)
318 if err != nil {
319 // nolint:staticcheck
320 caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err)
321 }
322 return match
323 }
324
325 // MatchWithError returns true if r matches m.
326 func (m MatchFile) MatchWithError(r *http.Request) (bool, error) {
327 return m.selectFile(r)
328 }
329
330 // selectFile chooses a file according to m.TryPolicy by appending
331 // the paths in m.TryFiles to m.Root, with placeholder replacements.
332 func (m MatchFile) selectFile(r *http.Request) (bool, error) {
333 repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
334
335 root := filepath.Clean(repl.ReplaceAll(m.Root, "."))
336
337 fsName := repl.ReplaceAll(m.FileSystem, "")
338
339 fileSystem, ok := m.fsmap.Get(fsName)
340 if !ok {
341 if c := m.logger.Check(zapcore.ErrorLevel, "use of unregistered filesystem"); c != nil {
342 c.Write(zap.String("fs", fsName))
343 }
344 return false, nil
345 }
346 type matchCandidate struct {
347 fullpath, relative, splitRemainder string
348 }
349
350 // makeCandidates evaluates placeholders in file and expands any glob expressions
351 // to build a list of file candidates. Special glob characters are escaped in
352 // placeholder replacements so globs cannot be expanded from placeholders, and
353 // globs are not evaluated on Windows because of its path separator character:
354 // escaping is not supported so we can't safely glob on Windows, or we can't
355 // support placeholders on Windows (pick one). (Actually, evaluating untrusted
356 // globs is not the end of the world since the file server will still hide any
357 // hidden files, it just might lead to unexpected behavior.)
358 makeCandidates := func(file string) []matchCandidate {
359 // first, evaluate placeholders in the file pattern
360 expandedFile, err := repl.ReplaceFunc(file, func(variable string, val any) (any, error) {
361 if runtime.GOOS == "windows" {
362 return val, nil
363 }
364 switch v := val.(type) {
365 case string:
366 return globSafeRepl.Replace(v), nil
367 case fmt.Stringer:
368 return globSafeRepl.Replace(v.String()), nil
369 }
370 return val, nil
371 })
372 if err != nil {
373 if c := m.logger.Check(zapcore.ErrorLevel, "evaluating placeholders"); c != nil {
374 c.Write(zap.Error(err))
375 }
376
377 expandedFile = file // "oh well," I guess?
378 }
379
380 // we must split before globbing so that the file system
381 // doesn't include the remainder ("afterSplit") in the filename
382 beforeSplit, afterSplit, ok := m.cleanSplit(file, expandedFile)
383 if !ok {
384 return nil
385 }
386
387 // create the full path to the file by prepending the site root
388 fullPattern := caddyhttp.SanitizedPathJoin(root, beforeSplit)
389
390 // expand glob expressions, but not on Windows because Glob() doesn't
391 // support escaping on Windows due to path separator)
392 var globResults []string
393 if runtime.GOOS == "windows" {
394 globResults = []string{fullPattern} // precious Windows
395 } else {
396 globResults, err = fs.Glob(fileSystem, fullPattern)
397 if err != nil {
398 if c := m.logger.Check(zapcore.ErrorLevel, "expanding glob"); c != nil {
399 c.Write(zap.Error(err))
400 }
401 }
402 }
403
404 // for each glob result, combine all the forms of the path
405 candidates := make([]matchCandidate, 0, len(globResults))
406 for _, result := range globResults {
407 candidates = append(candidates, matchCandidate{
408 fullpath: result,
409 relative: strings.TrimPrefix(result, root),
410 splitRemainder: afterSplit,
411 })
412 }
413
414 return candidates
415 }
416
417 // makeLiteralCandidate builds a candidate from file as a literal path,
418 // without expanding globs or checking that the file exists. It is used only
419 // for the final entry of first_exist_fallback, which matches unconditionally
420 // even when globbing found nothing because the file is absent.
421 makeLiteralCandidate := func(file string) (matchCandidate, bool) {
422 // placeholders are not glob-escaped here, since nothing is globbed;
423 // errors were already logged by makeCandidates for the same pattern
424 expandedFile, err := repl.ReplaceFunc(file, func(_ string, val any) (any, error) {
425 return val, nil
426 })
427 if err != nil {
428 expandedFile = file
429 }
430 beforeSplit, afterSplit, ok := m.cleanSplit(file, expandedFile)
431 if !ok {
432 return matchCandidate{}, false
433 }
434 fullPath := caddyhttp.SanitizedPathJoin(root, beforeSplit)
435 return matchCandidate{
436 fullpath: fullPath,
437 relative: strings.TrimPrefix(fullPath, root),
438 splitRemainder: afterSplit,
439 }, true
440 }
441
442 // setPlaceholders creates the placeholders for the matched file
443 setPlaceholders := func(candidate matchCandidate, isDir bool) {
444 repl.Set("http.matchers.file.relative", filepath.ToSlash(candidate.relative))
445 repl.Set("http.matchers.file.absolute", filepath.ToSlash(candidate.fullpath))
446 repl.Set("http.matchers.file.remainder", filepath.ToSlash(candidate.splitRemainder))
447
448 fileType := "file"
449 if isDir {
450 fileType = "directory"
451 }
452 repl.Set("http.matchers.file.type", fileType)
453 }
454
455 // match file according to the configured policy
456 switch m.TryPolicy {
457 case "", tryPolicyFirstExist, tryPolicyFirstExistFallback:
458 maxI := -1
459 if m.TryPolicy == tryPolicyFirstExistFallback {
460 maxI = len(m.TryFiles) - 1
461 }
462
463 for i, pattern := range m.TryFiles {
464 // If the pattern is a status code, emit an error,
465 // which short-circuits the middleware pipeline and
466 // writes an HTTP error response.
467 if err := parseErrorCode(pattern); err != nil {
468 return false, err
469 }
470
471 candidates := makeCandidates(pattern)
472 // globbing finds only files that exist, but the fallback entry matches anyway
473 if i == maxI && len(candidates) == 0 {
474 if c, ok := makeLiteralCandidate(pattern); ok {
475 candidates = []matchCandidate{c}
476 }
477 }
478 for _, c := range candidates {
479 // Skip the IO if using fallback policy and it's the latest item
480 if i == maxI {
481 setPlaceholders(c, false)
482
483 return true, nil
484 }
485
486 if info, exists := m.strictFileExists(fileSystem, c.fullpath); exists {
487 setPlaceholders(c, info.IsDir())
488 return true, nil
489 }
490 }
491 }
492
493 case tryPolicyLargestSize:
494 var largestSize int64
495 var largest matchCandidate
496 var largestInfo os.FileInfo
497 for _, pattern := range m.TryFiles {
498 candidates := makeCandidates(pattern)
499 for _, c := range candidates {
500 info, err := fs.Stat(fileSystem, c.fullpath)
501 if err == nil && (largestInfo == nil || info.Size() > largestSize) {
502 largestSize = info.Size()
503 largest = c
504 largestInfo = info
505 }
506 }
507 }
508 if largestInfo == nil {
509 return false, nil
510 }
511 setPlaceholders(largest, largestInfo.IsDir())
512 return true, nil
513
514 case tryPolicySmallestSize:
515 var smallestSize int64
516 var smallest matchCandidate
517 var smallestInfo os.FileInfo
518 for _, pattern := range m.TryFiles {
519 candidates := makeCandidates(pattern)
520 for _, c := range candidates {
521 info, err := fs.Stat(fileSystem, c.fullpath)
522 if err == nil && (smallestInfo == nil || info.Size() < smallestSize) {
523 smallestSize = info.Size()
524 smallest = c
525 smallestInfo = info
526 }
527 }
528 }
529 if smallestInfo == nil {
530 return false, nil
531 }
532 setPlaceholders(smallest, smallestInfo.IsDir())
533 return true, nil
534
535 case tryPolicyMostRecentlyMod:
536 var recent matchCandidate
537 var recentInfo os.FileInfo
538 for _, pattern := range m.TryFiles {
539 candidates := makeCandidates(pattern)
540 for _, c := range candidates {
541 info, err := fs.Stat(fileSystem, c.fullpath)
542 if err == nil &&
543 (recentInfo == nil || info.ModTime().After(recentInfo.ModTime())) {
544 recent = c
545 recentInfo = info
546 }
547 }
548 }
549 if recentInfo == nil {
550 return false, nil
551 }
552 setPlaceholders(recent, recentInfo.IsDir())
553 return true, nil
554 }
555
556 return false, nil
557 }
558
559 // parseErrorCode checks if the input is a status
560 // code number, prefixed by "=", and returns an
561 // error if so.
562 func parseErrorCode(input string) error {
563 if len(input) > 1 && input[0] == '=' {
564 code, err := strconv.Atoi(input[1:])
565 if err != nil || code < 100 || code > 999 {
566 return nil
567 }
568 return caddyhttp.Error(code, fmt.Errorf("%s", input[1:]))
569 }
570 return nil
571 }
572
573 // strictFileExists returns true if file exists
574 // and matches the convention of the given file
575 // path. If the path ends in a forward slash,
576 // the file must also be a directory; if it does
577 // NOT end in a forward slash, the file must NOT
578 // be a directory.
579 func (m MatchFile) strictFileExists(fileSystem fs.FS, file string) (os.FileInfo, bool) {
580 info, err := fs.Stat(fileSystem, file)
581 if err != nil {
582 // in reality, this can be any error
583 // such as permission or even obscure
584 // ones like "is not a directory" (when
585 // trying to stat a file within a file);
586 // in those cases we can't be sure if
587 // the file exists, so we just treat any
588 // error as if it does not exist; see
589 // https://stackoverflow.com/a/12518877/1048862
590 return nil, false
591 }
592 if strings.HasSuffix(file, separator) {
593 // by convention, file paths ending
594 // in a path separator must be a directory
595 return info, info.IsDir()
596 }
597 // by convention, file paths NOT ending
598 // in a path separator must NOT be a directory
599 return info, !info.IsDir()
600 }
601
602 // cleanSplit cleans expanded, the expansion of pattern, and splits it if
603 // configured. It keeps the trailing slash so that "/file/" or "/file/./"
604 // cannot match a regular file, and reports false for a path ending in "/"
605 // unless pattern does too, since only such patterns match a directory.
606 func (m MatchFile) cleanSplit(pattern, expanded string) (beforeSplit, afterSplit string, ok bool) {
607 cleaned := caddyhttp.NormalizedPath(expanded, true)
608 if cleaned == "" {
609 // Preserve path.Clean's existing result for an empty path.
610 cleaned = "."
611 }
612 beforeSplit, afterSplit = m.firstSplit(cleaned)
613 patternHasTrailingSlash := strings.HasSuffix(pattern, "/")
614 // SanitizedPathJoin cleans the doubled slash but uses it to preserve
615 // the filesystem separator after the site root.
616 if beforeSplit == "/" && patternHasTrailingSlash {
617 beforeSplit += "/"
618 }
619
620 // with "{path}", a request for "/dir/" falls through to the next entry (e.g. "{path}/index.php")
621 if beforeSplit != "/" && strings.HasSuffix(beforeSplit, "/") && !patternHasTrailingSlash {
622 return "", "", false
623 }
624 return beforeSplit, afterSplit, true
625 }
626
627 // firstSplit returns the first result where the path
628 // can be split in two by a value in m.SplitPath. The
629 // return values are the first piece of the path that
630 // ends with the split substring and the remainder.
631 // If the path cannot be split, the path is returned
632 // as-is (with no remainder).
633 func (m MatchFile) firstSplit(path string) (splitPart, remainder string) {
634 for _, split := range m.SplitPath {
635 if idx := indexFold(path, split); idx > -1 {
636 pos := idx + len(split)
637 // skip the split if it's not the final part of the filename
638 if pos != len(path) && !strings.HasPrefix(path[pos:], "/") {
639 continue
640 }
641 return path[:pos], path[pos:]
642 }
643 }
644 return path, ""
645 }
646
647 // There is no strings.IndexFold() function like there is strings.EqualFold(),
648 // but we can use strings.EqualFold() to build our own case-insensitive
649 // substring search (as of Go 1.14).
650 func indexFold(haystack, needle string) int {
651 nlen := len(needle)
652 for i := 0; i+nlen < len(haystack); i++ {
653 if strings.EqualFold(haystack[i:i+nlen], needle) {
654 return i
655 }
656 }
657 return -1
658 }
659
660 // isCELTryFilesLiteral returns whether the expression resolves to a map literal containing
661 // only string keys with or a placeholder call.
662 func isCELTryFilesLiteral(e ast.Expr) bool {
663 switch e.Kind() {
664 case ast.MapKind:
665 mapExpr := e.AsMap()
666 for _, entry := range mapExpr.Entries() {
667 mapKey := entry.AsMapEntry().Key()
668 mapVal := entry.AsMapEntry().Value()
669 if !isCELStringLiteral(mapKey) {
670 return false
671 }
672 mapKeyStr := mapKey.AsLiteral().ConvertToType(types.StringType).Value()
673 switch mapKeyStr {
674 case "try_files", "split_path":
675 if !isCELStringListLiteral(mapVal) {
676 return false
677 }
678 case "try_policy", "root":
679 if !isCELStringExpr(mapVal) {
680 return false
681 }
682 default:
683 return false
684 }
685 }
686 return true
687
688 case ast.UnspecifiedExprKind, ast.CallKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.SelectKind, ast.StructKind:
689 // appeasing the linter :)
690 }
691 return false
692 }
693
694 // isCELStringExpr indicates whether the expression is a supported string expression
695 func isCELStringExpr(e ast.Expr) bool {
696 return isCELStringLiteral(e) || isCELCaddyPlaceholderCall(e) || isCELConcatCall(e)
697 }
698
699 // isCELStringLiteral returns whether the expression is a CEL string literal.
700 func isCELStringLiteral(e ast.Expr) bool {
701 switch e.Kind() {
702 case ast.LiteralKind:
703 constant := e.AsLiteral()
704 switch constant.Type() {
705 case types.StringType:
706 return true
707 }
708 case ast.UnspecifiedExprKind, ast.CallKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.MapKind, ast.SelectKind, ast.StructKind:
709 // appeasing the linter :)
710 }
711 return false
712 }
713
714 // isCELCaddyPlaceholderCall returns whether the expression is a caddy placeholder call.
715 func isCELCaddyPlaceholderCall(e ast.Expr) bool {
716 switch e.Kind() {
717 case ast.CallKind:
718 call := e.AsCall()
719 if call.FunctionName() == caddyhttp.CELPlaceholderFuncName {
720 return true
721 }
722 case ast.UnspecifiedExprKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind:
723 // appeasing the linter :)
724 }
725 return false
726 }
727
728 // isCELConcatCall tests whether the expression is a concat function (+) with string, placeholder, or
729 // other concat call arguments.
730 func isCELConcatCall(e ast.Expr) bool {
731 switch e.Kind() {
732 case ast.CallKind:
733 call := e.AsCall()
734 if call.Target().Kind() != ast.UnspecifiedExprKind {
735 return false
736 }
737 if call.FunctionName() != operators.Add {
738 return false
739 }
740 for _, arg := range call.Args() {
741 if !isCELStringExpr(arg) {
742 return false
743 }
744 }
745 return true
746 case ast.UnspecifiedExprKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind:
747 // appeasing the linter :)
748 }
749 return false
750 }
751
752 // isCELStringListLiteral returns whether the expression resolves to a list literal
753 // containing only string constants or a placeholder call.
754 func isCELStringListLiteral(e ast.Expr) bool {
755 switch e.Kind() {
756 case ast.ListKind:
757 list := e.AsList()
758 for _, elem := range list.Elements() {
759 if !isCELStringExpr(elem) {
760 return false
761 }
762 }
763 return true
764 case ast.UnspecifiedExprKind, ast.CallKind, ast.ComprehensionKind, ast.IdentKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind:
765 // appeasing the linter :)
766 }
767 return false
768 }
769
770 // globSafeRepl replaces special glob characters with escaped
771 // equivalents. Note that the filepath godoc states that
772 // escaping is not done on Windows because of the separator.
773 var globSafeRepl = strings.NewReplacer(
774 "*", "\\*",
775 "[", "\\[",
776 "?", "\\?",
777 "\\", "\\\\",
778 )
779
780 const (
781 tryPolicyFirstExist = "first_exist"
782 tryPolicyFirstExistFallback = "first_exist_fallback"
783 tryPolicyLargestSize = "largest_size"
784 tryPolicySmallestSize = "smallest_size"
785 tryPolicyMostRecentlyMod = "most_recently_modified"
786 )
787
788 // Interface guards
789 var (
790 _ caddy.Validator = (*MatchFile)(nil)
791 _ caddyhttp.RequestMatcherWithError = (*MatchFile)(nil)
792 _ caddyhttp.CELLibraryProducer = (*MatchFile)(nil)
793 )