| 
									
										
										
										
											2019-06-30 16:07:58 -06:00
										 |  |  | // Copyright 2015 Matthew Holt and The Caddy Authors | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // 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. | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-20 21:21:33 -06:00
										 |  |  | package fileserver | 
					
						
							| 
									
										
										
										
											2019-05-20 10:59:20 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | 	"fmt" | 
					
						
							| 
									
										
										
										
											2022-07-30 13:07:44 -06:00
										 |  |  | 	"io/fs" | 
					
						
							| 
									
										
										
										
											2019-05-20 10:59:20 -06:00
										 |  |  | 	"net/http" | 
					
						
							|  |  |  | 	"os" | 
					
						
							| 
									
										
										
										
											2019-09-06 12:36:45 -06:00
										 |  |  | 	"path" | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 	"path/filepath" | 
					
						
							|  |  |  | 	"runtime" | 
					
						
							| 
									
										
										
										
											2021-09-17 02:52:32 -04:00
										 |  |  | 	"strconv" | 
					
						
							| 
									
										
										
										
											2019-09-06 13:32:02 -06:00
										 |  |  | 	"strings" | 
					
						
							| 
									
										
										
										
											2019-05-20 10:59:20 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 	"github.com/google/cel-go/cel" | 
					
						
							|  |  |  | 	"github.com/google/cel-go/common" | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 	"github.com/google/cel-go/common/ast" | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 	"github.com/google/cel-go/common/operators" | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 	"github.com/google/cel-go/common/types" | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 	"github.com/google/cel-go/common/types/ref" | 
					
						
							|  |  |  | 	"github.com/google/cel-go/parser" | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 	"go.uber.org/zap" | 
					
						
							| 
									
										
										
										
											2023-08-14 23:41:15 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/caddyserver/caddy/v2" | 
					
						
							|  |  |  | 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" | 
					
						
							|  |  |  | 	"github.com/caddyserver/caddy/v2/modules/caddyhttp" | 
					
						
							| 
									
										
										
										
											2019-05-20 10:59:20 -06:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func init() { | 
					
						
							| 
									
										
										
										
											2019-08-21 10:46:35 -06:00
										 |  |  | 	caddy.RegisterModule(MatchFile{}) | 
					
						
							| 
									
										
										
										
											2019-05-20 10:59:20 -06:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | // MatchFile is an HTTP request matcher that can match | 
					
						
							|  |  |  | // requests based upon file existence. | 
					
						
							| 
									
										
										
										
											2019-12-29 13:16:34 -07:00
										 |  |  | // | 
					
						
							| 
									
										
										
										
											2020-09-16 20:09:28 -04:00
										 |  |  | // Upon matching, three new placeholders will be made | 
					
						
							| 
									
										
										
										
											2019-12-29 13:16:34 -07:00
										 |  |  | // available: | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // - `{http.matchers.file.relative}` The root-relative | 
					
						
							|  |  |  | // path of the file. This is often useful when rewriting | 
					
						
							|  |  |  | // requests. | 
					
						
							|  |  |  | // - `{http.matchers.file.absolute}` The absolute path | 
					
						
							|  |  |  | // of the matched file. | 
					
						
							| 
									
										
										
										
											2020-09-16 20:09:28 -04:00
										 |  |  | // - `{http.matchers.file.type}` Set to "directory" if | 
					
						
							|  |  |  | // the matched file is a directory, "file" otherwise. | 
					
						
							| 
									
										
										
										
											2020-12-04 19:12:13 -05:00
										 |  |  | // - `{http.matchers.file.remainder}` Set to the remainder | 
					
						
							|  |  |  | // of the path if the path was split by `split_path`. | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | // | 
					
						
							|  |  |  | // Even though file matching may depend on the OS path | 
					
						
							|  |  |  | // separator, the placeholder values always use /. | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | type MatchFile struct { | 
					
						
							| 
									
										
										
										
											2022-07-30 13:07:44 -06:00
										 |  |  | 	// The file system implementation to use. By default, the | 
					
						
							|  |  |  | 	// local disk file system will be used. | 
					
						
							| 
									
										
										
										
											2024-01-13 14:12:43 -06:00
										 |  |  | 	FileSystem string `json:"fs,omitempty"` | 
					
						
							| 
									
										
										
										
											2022-07-30 13:07:44 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | 	// The root directory, used for creating absolute | 
					
						
							|  |  |  | 	// file paths, and required when working with | 
					
						
							| 
									
										
										
										
											2019-12-29 13:16:34 -07:00
										 |  |  | 	// relative paths; if not specified, `{http.vars.root}` | 
					
						
							|  |  |  | 	// will be used, if set; otherwise, the current | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | 	// directory is assumed. Accepts placeholders. | 
					
						
							|  |  |  | 	Root string `json:"root,omitempty"` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// The list of files to try. Each path here is | 
					
						
							| 
									
										
										
										
											2020-02-28 10:30:48 +08:00
										 |  |  | 	// considered related to Root. If nil, the request | 
					
						
							| 
									
										
										
										
											2020-01-22 09:32:38 -07:00
										 |  |  | 	// URL's path will be assumed. Files and | 
					
						
							|  |  |  | 	// directories are treated distinctly, so to match | 
					
						
							|  |  |  | 	// a directory, the filepath MUST end in a forward | 
					
						
							|  |  |  | 	// slash `/`. To match a regular file, there must | 
					
						
							| 
									
										
										
										
											2021-09-17 02:52:32 -04:00
										 |  |  | 	// be no trailing slash. Accepts placeholders. If | 
					
						
							|  |  |  | 	// the policy is "first_exist", then an error may | 
					
						
							|  |  |  | 	// be triggered as a fallback by configuring "=" | 
					
						
							|  |  |  | 	// followed by a status code number, | 
					
						
							|  |  |  | 	// for example "=404". | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | 	TryFiles []string `json:"try_files,omitempty"` | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-23 12:45:35 -07:00
										 |  |  | 	// How to choose a file in TryFiles. Can be: | 
					
						
							|  |  |  | 	// | 
					
						
							|  |  |  | 	// - first_exist | 
					
						
							|  |  |  | 	// - smallest_size | 
					
						
							|  |  |  | 	// - largest_size | 
					
						
							|  |  |  | 	// - most_recently_modified | 
					
						
							|  |  |  | 	// | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | 	// Default is first_exist. | 
					
						
							|  |  |  | 	TryPolicy string `json:"try_policy,omitempty"` | 
					
						
							| 
									
										
										
										
											2020-04-27 16:46:46 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// A list of delimiters to use to split the path in two | 
					
						
							|  |  |  | 	// when trying files. If empty, no splitting will | 
					
						
							|  |  |  | 	// occur, and the path will be tried as-is. For each | 
					
						
							|  |  |  | 	// split value, the left-hand side of the split, | 
					
						
							|  |  |  | 	// including the split value, will be the path tried. | 
					
						
							|  |  |  | 	// For example, the path `/remote.php/dav/` using the | 
					
						
							|  |  |  | 	// split value `.php` would try the file `/remote.php`. | 
					
						
							|  |  |  | 	// Each delimiter must appear at the end of a URI path | 
					
						
							|  |  |  | 	// component in order to be used as a split delimiter. | 
					
						
							|  |  |  | 	SplitPath []string `json:"split_path,omitempty"` | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-13 14:12:43 -06:00
										 |  |  | 	fsmap caddy.FileSystems | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 	logger *zap.Logger | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-21 10:46:35 -06:00
										 |  |  | // CaddyModule returns the Caddy module information. | 
					
						
							|  |  |  | func (MatchFile) CaddyModule() caddy.ModuleInfo { | 
					
						
							|  |  |  | 	return caddy.ModuleInfo{ | 
					
						
							| 
									
										
										
										
											2019-12-10 13:36:46 -07:00
										 |  |  | 		ID:  "http.matchers.file", | 
					
						
							|  |  |  | 		New: func() caddy.Module { return new(MatchFile) }, | 
					
						
							| 
									
										
										
										
											2019-08-21 10:46:35 -06:00
										 |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-09 12:05:47 -06:00
										 |  |  | // UnmarshalCaddyfile sets up the matcher from Caddyfile tokens. Syntax: | 
					
						
							|  |  |  | // | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | //	file <files...> { | 
					
						
							|  |  |  | //	    root      <path> | 
					
						
							|  |  |  | //	    try_files <files...> | 
					
						
							|  |  |  | //	    try_policy first_exist|smallest_size|largest_size|most_recently_modified | 
					
						
							|  |  |  | //	} | 
					
						
							| 
									
										
										
										
											2019-08-09 12:05:47 -06:00
										 |  |  | func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { | 
					
						
							| 
									
										
										
										
											2024-01-23 19:36:59 -05:00
										 |  |  | 	// iterate to merge multiple matchers into one | 
					
						
							| 
									
										
										
										
											2019-08-09 12:05:47 -06:00
										 |  |  | 	for d.Next() { | 
					
						
							| 
									
										
										
										
											2020-05-05 12:34:58 -06:00
										 |  |  | 		m.TryFiles = append(m.TryFiles, d.RemainingArgs()...) | 
					
						
							| 
									
										
										
										
											2019-09-10 19:21:52 -06:00
										 |  |  | 		for d.NextBlock(0) { | 
					
						
							| 
									
										
										
										
											2019-08-09 12:05:47 -06:00
										 |  |  | 			switch d.Val() { | 
					
						
							|  |  |  | 			case "root": | 
					
						
							|  |  |  | 				if !d.NextArg() { | 
					
						
							|  |  |  | 					return d.ArgErr() | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				m.Root = d.Val() | 
					
						
							|  |  |  | 			case "try_files": | 
					
						
							| 
									
										
										
										
											2020-05-05 12:34:58 -06:00
										 |  |  | 				m.TryFiles = append(m.TryFiles, d.RemainingArgs()...) | 
					
						
							| 
									
										
										
										
											2019-08-09 12:05:47 -06:00
										 |  |  | 				if len(m.TryFiles) == 0 { | 
					
						
							|  |  |  | 					return d.ArgErr() | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			case "try_policy": | 
					
						
							|  |  |  | 				if !d.NextArg() { | 
					
						
							|  |  |  | 					return d.ArgErr() | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				m.TryPolicy = d.Val() | 
					
						
							| 
									
										
										
										
											2020-07-31 13:55:01 -06:00
										 |  |  | 			case "split_path": | 
					
						
							| 
									
										
										
										
											2020-04-27 16:46:46 -04:00
										 |  |  | 				m.SplitPath = d.RemainingArgs() | 
					
						
							|  |  |  | 				if len(m.SplitPath) == 0 { | 
					
						
							|  |  |  | 					return d.ArgErr() | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2020-07-31 13:55:01 -06:00
										 |  |  | 			default: | 
					
						
							|  |  |  | 				return d.Errf("unrecognized subdirective: %s", d.Val()) | 
					
						
							| 
									
										
										
										
											2019-08-09 12:05:47 -06:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2019-09-06 12:36:45 -06:00
										 |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | // CELLibrary produces options that expose this matcher for use in CEL | 
					
						
							|  |  |  | // expression matchers. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // Example: | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | // | 
					
						
							| 
									
										
										
										
											2023-02-08 12:49:17 -05:00
										 |  |  | //	expression file() | 
					
						
							|  |  |  | //	expression file({http.request.uri.path}, '/index.php') | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | //	expression file({'root': '/srv', 'try_files': [{http.request.uri.path}, '/index.php'], 'try_policy': 'first_exist', 'split_path': ['.php']}) | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) { | 
					
						
							| 
									
										
										
										
											2022-07-28 14:50:28 -06:00
										 |  |  | 	requestType := cel.ObjectType("http.Request") | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcher, error) { | 
					
						
							|  |  |  | 		values, err := caddyhttp.CELValueToMapStrList(data) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		var root string | 
					
						
							|  |  |  | 		if len(values["root"]) > 0 { | 
					
						
							|  |  |  | 			root = values["root"][0] | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-13 14:12:43 -06:00
										 |  |  | 		var fsName string | 
					
						
							|  |  |  | 		if len(values["fs"]) > 0 { | 
					
						
							|  |  |  | 			fsName = values["fs"][0] | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 		var try_policy string | 
					
						
							|  |  |  | 		if len(values["try_policy"]) > 0 { | 
					
						
							|  |  |  | 			root = values["try_policy"][0] | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		m := MatchFile{ | 
					
						
							| 
									
										
										
										
											2024-01-13 14:12:43 -06:00
										 |  |  | 			Root:       root, | 
					
						
							|  |  |  | 			TryFiles:   values["try_files"], | 
					
						
							|  |  |  | 			TryPolicy:  try_policy, | 
					
						
							|  |  |  | 			SplitPath:  values["split_path"], | 
					
						
							|  |  |  | 			FileSystem: fsName, | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		err = m.Provision(ctx) | 
					
						
							|  |  |  | 		return m, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-28 15:16:36 -06:00
										 |  |  | 	envOptions := []cel.EnvOption{ | 
					
						
							|  |  |  | 		cel.Macros(parser.NewGlobalVarArgMacro("file", celFileMatcherMacroExpander())), | 
					
						
							|  |  |  | 		cel.Function("file", cel.Overload("file_request_map", []*cel.Type{requestType, caddyhttp.CELTypeJSON}, cel.BoolType)), | 
					
						
							|  |  |  | 		cel.Function("file_request_map", | 
					
						
							|  |  |  | 			cel.Overload("file_request_map", []*cel.Type{requestType, caddyhttp.CELTypeJSON}, cel.BoolType), | 
					
						
							| 
									
										
										
										
											2023-02-08 12:49:17 -05:00
										 |  |  | 			cel.SingletonBinaryBinding(caddyhttp.CELMatcherRuntimeFunction("file_request_map", matcherFactory))), | 
					
						
							| 
									
										
										
										
											2022-07-28 15:16:36 -06:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 	programOptions := []cel.ProgramOption{ | 
					
						
							|  |  |  | 		cel.CustomDecorator(caddyhttp.CELMatcherDecorator("file_request_map", matcherFactory)), | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return caddyhttp.NewMatcherCELLibrary(envOptions, programOptions), nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func celFileMatcherMacroExpander() parser.MacroExpander { | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 	return func(eh parser.ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) { | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 		if len(args) == 0 { | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 			return eh.NewCall("file", | 
					
						
							|  |  |  | 				eh.NewIdent("request"), | 
					
						
							| 
									
										
										
										
											2023-02-08 12:49:17 -05:00
										 |  |  | 				eh.NewMap(), | 
					
						
							|  |  |  | 			), nil | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 		} | 
					
						
							|  |  |  | 		if len(args) == 1 { | 
					
						
							|  |  |  | 			arg := args[0] | 
					
						
							|  |  |  | 			if isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg) { | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 				return eh.NewCall("file", | 
					
						
							|  |  |  | 					eh.NewIdent("request"), | 
					
						
							| 
									
										
										
										
											2023-02-08 12:49:17 -05:00
										 |  |  | 					eh.NewMap(eh.NewMapEntry( | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 						eh.NewLiteral(types.String("try_files")), | 
					
						
							| 
									
										
										
										
											2023-02-08 12:49:17 -05:00
										 |  |  | 						eh.NewList(arg), | 
					
						
							|  |  |  | 						false, | 
					
						
							|  |  |  | 					)), | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 				), nil | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if isCELTryFilesLiteral(arg) { | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 				return eh.NewCall("file", eh.NewIdent("request"), arg), nil | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 			} | 
					
						
							|  |  |  | 			return nil, &common.Error{ | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 				Location: eh.OffsetLocation(arg.ID()), | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 				Message:  "matcher requires either a map or string literal argument", | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		for _, arg := range args { | 
					
						
							|  |  |  | 			if !(isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg)) { | 
					
						
							|  |  |  | 				return nil, &common.Error{ | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 					Location: eh.OffsetLocation(arg.ID()), | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 					Message:  "matcher only supports repeated string literal arguments", | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 		return eh.NewCall("file", | 
					
						
							|  |  |  | 			eh.NewIdent("request"), | 
					
						
							| 
									
										
										
										
											2023-02-08 12:49:17 -05:00
										 |  |  | 			eh.NewMap(eh.NewMapEntry( | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 				eh.NewLiteral(types.String("try_files")), | 
					
						
							| 
									
										
										
										
											2023-02-08 12:49:17 -05:00
										 |  |  | 				eh.NewList(args...), | 
					
						
							|  |  |  | 				false, | 
					
						
							|  |  |  | 			)), | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 		), nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-06 12:36:45 -06:00
										 |  |  | // Provision sets up m's defaults. | 
					
						
							| 
									
										
										
										
											2022-07-30 13:07:44 -06:00
										 |  |  | func (m *MatchFile) Provision(ctx caddy.Context) error { | 
					
						
							| 
									
										
										
										
											2022-09-16 16:55:30 -06:00
										 |  |  | 	m.logger = ctx.Logger() | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-13 14:12:43 -06:00
										 |  |  | 	m.fsmap = ctx.Filesystems() | 
					
						
							| 
									
										
										
										
											2022-07-30 13:07:44 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-21 10:46:35 -06:00
										 |  |  | 	if m.Root == "" { | 
					
						
							| 
									
										
										
										
											2019-09-06 12:36:45 -06:00
										 |  |  | 		m.Root = "{http.vars.root}" | 
					
						
							| 
									
										
										
										
											2019-08-21 10:46:35 -06:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-07-30 13:07:44 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-13 14:12:43 -06:00
										 |  |  | 	if m.FileSystem == "" { | 
					
						
							|  |  |  | 		m.FileSystem = "{http.vars.fs}" | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
											  
											
												fileserver: Fix `file` matcher with empty `try_files` (#4147)
* fileserver: Fix `file` matcher with empty `try_files`
Fixes https://github.com/caddyserver/caddy/issues/4146
If `TryFiles` is empty, we fill it with `r.URL.Path`. In this case, this is `/`. Then later, in `prepareFilePath()`, we run the replacer (which turns `{path}` into `/` at that point) but `file` remains the original value (and the placeholder is still the placeholder there).
So then `strings.HasSuffix(file, "/")` will be `false` for the placeholder, but `true` for the empty `TryFiles` codepath, because `file` was `/` due to being set to the actual request value beforehand.
This means that `suffix` becomes `//` in that case, so after `sanitizedPathJoin`, it becomes `./`, so `strictFileExists`'s `strings.HasSuffix(file, separator)` codepath will return true.
I think we should change the `m.TryFiles == nil` codepath to `m.TryFiles = []string{"{http.request.uri.path}"}` for consistency. (And maybe consider hoisting this to `Provision` cause there's no point doing this on every request). I don't think this "optimization" of directly using `r.URL.Path` is so valuable, cause it causes this edgecase with directories.
* Update modules/caddyhttp/fileserver/matcher.go
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
											
										 
											2021-05-04 11:49:13 -04:00
										 |  |  | 	// if list of files to try was omitted entirely, assume URL path | 
					
						
							|  |  |  | 	// (use placeholder instead of r.URL.Path; see issue #4146) | 
					
						
							|  |  |  | 	if m.TryFiles == nil { | 
					
						
							|  |  |  | 		m.TryFiles = []string{"{http.request.uri.path}"} | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2019-08-09 12:05:47 -06:00
										 |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | // Validate ensures m has a valid configuration. | 
					
						
							|  |  |  | func (m MatchFile) Validate() error { | 
					
						
							|  |  |  | 	switch m.TryPolicy { | 
					
						
							|  |  |  | 	case "", | 
					
						
							|  |  |  | 		tryPolicyFirstExist, | 
					
						
							|  |  |  | 		tryPolicyLargestSize, | 
					
						
							|  |  |  | 		tryPolicySmallestSize, | 
					
						
							| 
									
										
										
										
											2019-12-23 12:45:35 -07:00
										 |  |  | 		tryPolicyMostRecentlyMod: | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | 	default: | 
					
						
							|  |  |  | 		return fmt.Errorf("unknown try policy %s", m.TryPolicy) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Match returns true if r matches m. Returns true | 
					
						
							| 
									
										
										
										
											2020-12-04 19:12:13 -05:00
										 |  |  | // if a file was matched. If so, four placeholders | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | // will be available: | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | //   - http.matchers.file.relative: Path to file relative to site root | 
					
						
							|  |  |  | //   - http.matchers.file.absolute: Path to file including site root | 
					
						
							|  |  |  | //   - http.matchers.file.type: file or directory | 
					
						
							|  |  |  | //   - http.matchers.file.remainder: Portion remaining after splitting file path (if configured) | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | func (m MatchFile) Match(r *http.Request) bool { | 
					
						
							| 
									
										
										
										
											2020-09-16 20:09:28 -04:00
										 |  |  | 	return m.selectFile(r) | 
					
						
							| 
									
										
										
										
											2019-05-20 10:59:20 -06:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | // selectFile chooses a file according to m.TryPolicy by appending | 
					
						
							|  |  |  | // the paths in m.TryFiles to m.Root, with placeholder replacements. | 
					
						
							| 
									
										
										
										
											2020-09-16 20:09:28 -04:00
										 |  |  | func (m MatchFile) selectFile(r *http.Request) (matched bool) { | 
					
						
							| 
									
										
										
										
											2019-12-29 13:12:52 -07:00
										 |  |  | 	repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 	root := filepath.Clean(repl.ReplaceAll(m.Root, ".")) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-13 14:12:43 -06:00
										 |  |  | 	fsName := repl.ReplaceAll(m.FileSystem, "") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	fileSystem, ok := m.fsmap.Get(fsName) | 
					
						
							|  |  |  | 	if !ok { | 
					
						
							|  |  |  | 		m.logger.Error("use of unregistered filesystem", zap.String("fs", fsName)) | 
					
						
							|  |  |  | 		return false | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 	type matchCandidate struct { | 
					
						
							|  |  |  | 		fullpath, relative, splitRemainder string | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 	// makeCandidates evaluates placeholders in file and expands any glob expressions | 
					
						
							|  |  |  | 	// to build a list of file candidates. Special glob characters are escaped in | 
					
						
							|  |  |  | 	// placeholder replacements so globs cannot be expanded from placeholders, and | 
					
						
							|  |  |  | 	// globs are not evaluated on Windows because of its path separator character: | 
					
						
							|  |  |  | 	// escaping is not supported so we can't safely glob on Windows, or we can't | 
					
						
							|  |  |  | 	// support placeholders on Windows (pick one). (Actually, evaluating untrusted | 
					
						
							|  |  |  | 	// globs is not the end of the world since the file server will still hide any | 
					
						
							|  |  |  | 	// hidden files, it just might lead to unexpected behavior.) | 
					
						
							|  |  |  | 	makeCandidates := func(file string) []matchCandidate { | 
					
						
							|  |  |  | 		// first, evaluate placeholders in the file pattern | 
					
						
							|  |  |  | 		expandedFile, err := repl.ReplaceFunc(file, func(variable string, val any) (any, error) { | 
					
						
							|  |  |  | 			if runtime.GOOS == "windows" { | 
					
						
							|  |  |  | 				return val, nil | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			switch v := val.(type) { | 
					
						
							|  |  |  | 			case string: | 
					
						
							|  |  |  | 				return globSafeRepl.Replace(v), nil | 
					
						
							|  |  |  | 			case fmt.Stringer: | 
					
						
							|  |  |  | 				return globSafeRepl.Replace(v.String()), nil | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			return val, nil | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			m.logger.Error("evaluating placeholders", zap.Error(err)) | 
					
						
							|  |  |  | 			expandedFile = file // "oh well," I guess? | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// clean the path and split, if configured -- we must split before | 
					
						
							|  |  |  | 		// globbing so that the file system doesn't include the remainder | 
					
						
							|  |  |  | 		// ("afterSplit") in the filename; be sure to restore trailing slash | 
					
						
							|  |  |  | 		beforeSplit, afterSplit := m.firstSplit(path.Clean(expandedFile)) | 
					
						
							| 
									
										
										
										
											2020-09-16 20:09:28 -04:00
										 |  |  | 		if strings.HasSuffix(file, "/") { | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 			beforeSplit += "/" | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// create the full path to the file by prepending the site root | 
					
						
							|  |  |  | 		fullPattern := caddyhttp.SanitizedPathJoin(root, beforeSplit) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// expand glob expressions, but not on Windows because Glob() doesn't | 
					
						
							|  |  |  | 		// support escaping on Windows due to path separator) | 
					
						
							|  |  |  | 		var globResults []string | 
					
						
							|  |  |  | 		if runtime.GOOS == "windows" { | 
					
						
							|  |  |  | 			globResults = []string{fullPattern} // precious Windows | 
					
						
							|  |  |  | 		} else { | 
					
						
							| 
									
										
										
										
											2024-01-13 14:12:43 -06:00
										 |  |  | 			globResults, err = fs.Glob(fileSystem, fullPattern) | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				m.logger.Error("expanding glob", zap.Error(err)) | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2020-09-16 20:09:28 -04:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// for each glob result, combine all the forms of the path | 
					
						
							|  |  |  | 		var candidates []matchCandidate | 
					
						
							|  |  |  | 		for _, result := range globResults { | 
					
						
							|  |  |  | 			candidates = append(candidates, matchCandidate{ | 
					
						
							|  |  |  | 				fullpath:       result, | 
					
						
							|  |  |  | 				relative:       strings.TrimPrefix(result, root), | 
					
						
							|  |  |  | 				splitRemainder: afterSplit, | 
					
						
							|  |  |  | 			}) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return candidates | 
					
						
							| 
									
										
										
										
											2020-09-16 20:09:28 -04:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 	// setPlaceholders creates the placeholders for the matched file | 
					
						
							|  |  |  | 	setPlaceholders := func(candidate matchCandidate, info fs.FileInfo) { | 
					
						
							|  |  |  | 		repl.Set("http.matchers.file.relative", filepath.ToSlash(candidate.relative)) | 
					
						
							|  |  |  | 		repl.Set("http.matchers.file.absolute", filepath.ToSlash(candidate.fullpath)) | 
					
						
							|  |  |  | 		repl.Set("http.matchers.file.remainder", filepath.ToSlash(candidate.splitRemainder)) | 
					
						
							| 
									
										
										
										
											2020-09-16 20:09:28 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		fileType := "file" | 
					
						
							|  |  |  | 		if info.IsDir() { | 
					
						
							|  |  |  | 			fileType = "directory" | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		repl.Set("http.matchers.file.type", fileType) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 	// match file according to the configured policy | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | 	switch m.TryPolicy { | 
					
						
							|  |  |  | 	case "", tryPolicyFirstExist: | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 		for _, pattern := range m.TryFiles { | 
					
						
							|  |  |  | 			if err := parseErrorCode(pattern); err != nil { | 
					
						
							| 
									
										
										
										
											2021-09-17 02:52:32 -04:00
										 |  |  | 				caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err) | 
					
						
							|  |  |  | 				return | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 			candidates := makeCandidates(pattern) | 
					
						
							|  |  |  | 			for _, c := range candidates { | 
					
						
							| 
									
										
										
										
											2024-01-13 14:12:43 -06:00
										 |  |  | 				if info, exists := m.strictFileExists(fileSystem, c.fullpath); exists { | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 					setPlaceholders(c, info) | 
					
						
							|  |  |  | 					return true | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2019-05-20 10:59:20 -06:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	case tryPolicyLargestSize: | 
					
						
							|  |  |  | 		var largestSize int64 | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 		var largest matchCandidate | 
					
						
							|  |  |  | 		var largestInfo os.FileInfo | 
					
						
							|  |  |  | 		for _, pattern := range m.TryFiles { | 
					
						
							|  |  |  | 			candidates := makeCandidates(pattern) | 
					
						
							|  |  |  | 			for _, c := range candidates { | 
					
						
							| 
									
										
										
										
											2024-01-13 14:12:43 -06:00
										 |  |  | 				info, err := fs.Stat(fileSystem, c.fullpath) | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 				if err == nil && info.Size() > largestSize { | 
					
						
							|  |  |  | 					largestSize = info.Size() | 
					
						
							|  |  |  | 					largest = c | 
					
						
							|  |  |  | 					largestInfo = info | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 		if largestInfo == nil { | 
					
						
							|  |  |  | 			return false | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		setPlaceholders(largest, largestInfo) | 
					
						
							| 
									
										
										
										
											2020-09-16 20:09:28 -04:00
										 |  |  | 		return true | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	case tryPolicySmallestSize: | 
					
						
							|  |  |  | 		var smallestSize int64 | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 		var smallest matchCandidate | 
					
						
							|  |  |  | 		var smallestInfo os.FileInfo | 
					
						
							|  |  |  | 		for _, pattern := range m.TryFiles { | 
					
						
							|  |  |  | 			candidates := makeCandidates(pattern) | 
					
						
							|  |  |  | 			for _, c := range candidates { | 
					
						
							| 
									
										
										
										
											2024-01-13 14:12:43 -06:00
										 |  |  | 				info, err := fs.Stat(fileSystem, c.fullpath) | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 				if err == nil && (smallestSize == 0 || info.Size() < smallestSize) { | 
					
						
							|  |  |  | 					smallestSize = info.Size() | 
					
						
							|  |  |  | 					smallest = c | 
					
						
							|  |  |  | 					smallestInfo = info | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 		if smallestInfo == nil { | 
					
						
							|  |  |  | 			return false | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		setPlaceholders(smallest, smallestInfo) | 
					
						
							| 
									
										
										
										
											2020-09-16 20:09:28 -04:00
										 |  |  | 		return true | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-23 12:45:35 -07:00
										 |  |  | 	case tryPolicyMostRecentlyMod: | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 		var recent matchCandidate | 
					
						
							|  |  |  | 		var recentInfo os.FileInfo | 
					
						
							|  |  |  | 		for _, pattern := range m.TryFiles { | 
					
						
							|  |  |  | 			candidates := makeCandidates(pattern) | 
					
						
							|  |  |  | 			for _, c := range candidates { | 
					
						
							| 
									
										
										
										
											2024-01-13 14:12:43 -06:00
										 |  |  | 				info, err := fs.Stat(fileSystem, c.fullpath) | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 				if err == nil && | 
					
						
							|  |  |  | 					(recentInfo == nil || info.ModTime().After(recentInfo.ModTime())) { | 
					
						
							|  |  |  | 					recent = c | 
					
						
							|  |  |  | 					recentInfo = info | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 		if recentInfo == nil { | 
					
						
							|  |  |  | 			return false | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		setPlaceholders(recent, recentInfo) | 
					
						
							| 
									
										
										
										
											2020-09-16 20:09:28 -04:00
										 |  |  | 		return true | 
					
						
							| 
									
										
										
										
											2019-05-20 10:59:20 -06:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	return | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-17 02:52:32 -04:00
										 |  |  | // parseErrorCode checks if the input is a status | 
					
						
							|  |  |  | // code number, prefixed by "=", and returns an | 
					
						
							|  |  |  | // error if so. | 
					
						
							|  |  |  | func parseErrorCode(input string) error { | 
					
						
							|  |  |  | 	if len(input) > 1 && input[0] == '=' { | 
					
						
							|  |  |  | 		code, err := strconv.Atoi(input[1:]) | 
					
						
							|  |  |  | 		if err != nil || code < 100 || code > 999 { | 
					
						
							|  |  |  | 			return nil | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return caddyhttp.Error(code, fmt.Errorf("%s", input[1:])) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-06 13:32:02 -06:00
										 |  |  | // strictFileExists returns true if file exists | 
					
						
							|  |  |  | // and matches the convention of the given file | 
					
						
							|  |  |  | // path. If the path ends in a forward slash, | 
					
						
							|  |  |  | // the file must also be a directory; if it does | 
					
						
							|  |  |  | // NOT end in a forward slash, the file must NOT | 
					
						
							|  |  |  | // be a directory. | 
					
						
							| 
									
										
										
										
											2024-01-13 14:12:43 -06:00
										 |  |  | func (m MatchFile) strictFileExists(fileSystem fs.FS, file string) (os.FileInfo, bool) { | 
					
						
							|  |  |  | 	info, err := fs.Stat(fileSystem, file) | 
					
						
							| 
									
										
										
										
											2019-09-06 13:32:02 -06:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		// in reality, this can be any error | 
					
						
							|  |  |  | 		// such as permission or even obscure | 
					
						
							|  |  |  | 		// ones like "is not a directory" (when | 
					
						
							|  |  |  | 		// trying to stat a file within a file); | 
					
						
							|  |  |  | 		// in those cases we can't be sure if | 
					
						
							|  |  |  | 		// the file exists, so we just treat any | 
					
						
							|  |  |  | 		// error as if it does not exist; see | 
					
						
							|  |  |  | 		// https://stackoverflow.com/a/12518877/1048862 | 
					
						
							| 
									
										
										
										
											2020-09-16 20:09:28 -04:00
										 |  |  | 		return nil, false | 
					
						
							| 
									
										
										
										
											2019-09-06 12:57:12 -06:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2020-11-02 14:20:12 -07:00
										 |  |  | 	if strings.HasSuffix(file, separator) { | 
					
						
							| 
									
										
										
										
											2019-09-06 13:32:02 -06:00
										 |  |  | 		// by convention, file paths ending | 
					
						
							| 
									
										
										
										
											2020-09-16 20:09:28 -04:00
										 |  |  | 		// in a path separator must be a directory | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 		return info, info.IsDir() | 
					
						
							| 
									
										
										
										
											2019-09-06 13:32:02 -06:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	// by convention, file paths NOT ending | 
					
						
							| 
									
										
										
										
											2020-09-16 20:09:28 -04:00
										 |  |  | 	// in a path separator must NOT be a directory | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | 	return info, !info.IsDir() | 
					
						
							| 
									
										
										
										
											2019-05-20 10:59:20 -06:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-27 16:46:46 -04:00
										 |  |  | // firstSplit returns the first result where the path | 
					
						
							|  |  |  | // can be split in two by a value in m.SplitPath. The | 
					
						
							| 
									
										
										
										
											2020-12-04 19:12:13 -05:00
										 |  |  | // return values are the first piece of the path that | 
					
						
							|  |  |  | // ends with the split substring and the remainder. | 
					
						
							|  |  |  | // If the path cannot be split, the path is returned | 
					
						
							|  |  |  | // as-is (with no remainder). | 
					
						
							|  |  |  | func (m MatchFile) firstSplit(path string) (splitPart, remainder string) { | 
					
						
							| 
									
										
										
										
											2020-04-27 16:46:46 -04:00
										 |  |  | 	for _, split := range m.SplitPath { | 
					
						
							| 
									
										
										
										
											2020-07-31 13:55:01 -06:00
										 |  |  | 		if idx := indexFold(path, split); idx > -1 { | 
					
						
							| 
									
										
										
										
											2020-04-27 16:46:46 -04:00
										 |  |  | 			pos := idx + len(split) | 
					
						
							|  |  |  | 			// skip the split if it's not the final part of the filename | 
					
						
							|  |  |  | 			if pos != len(path) && !strings.HasPrefix(path[pos:], "/") { | 
					
						
							|  |  |  | 				continue | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2020-12-04 19:12:13 -05:00
										 |  |  | 			return path[:pos], path[pos:] | 
					
						
							| 
									
										
										
										
											2020-04-27 16:46:46 -04:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2020-12-04 19:12:13 -05:00
										 |  |  | 	return path, "" | 
					
						
							| 
									
										
										
										
											2020-04-27 16:46:46 -04:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-31 13:55:01 -06:00
										 |  |  | // There is no strings.IndexFold() function like there is strings.EqualFold(), | 
					
						
							|  |  |  | // but we can use strings.EqualFold() to build our own case-insensitive | 
					
						
							|  |  |  | // substring search (as of Go 1.14). | 
					
						
							|  |  |  | func indexFold(haystack, needle string) int { | 
					
						
							|  |  |  | 	nlen := len(needle) | 
					
						
							|  |  |  | 	for i := 0; i+nlen < len(haystack); i++ { | 
					
						
							|  |  |  | 		if strings.EqualFold(haystack[i:i+nlen], needle) { | 
					
						
							|  |  |  | 			return i | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return -1 | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-25 23:54:42 +08:00
										 |  |  | // isCELTryFilesLiteral returns whether the expression resolves to a map literal containing | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | // only string keys with or a placeholder call. | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | func isCELTryFilesLiteral(e ast.Expr) bool { | 
					
						
							|  |  |  | 	switch e.Kind() { | 
					
						
							|  |  |  | 	case ast.MapKind: | 
					
						
							|  |  |  | 		mapExpr := e.AsMap() | 
					
						
							|  |  |  | 		for _, entry := range mapExpr.Entries() { | 
					
						
							|  |  |  | 			mapKey := entry.AsMapEntry().Key() | 
					
						
							|  |  |  | 			mapVal := entry.AsMapEntry().Value() | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 			if !isCELStringLiteral(mapKey) { | 
					
						
							|  |  |  | 				return false | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 			mapKeyStr := mapKey.AsLiteral().ConvertToType(types.StringType).Value() | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 			if mapKeyStr == "try_files" || mapKeyStr == "split_path" { | 
					
						
							|  |  |  | 				if !isCELStringListLiteral(mapVal) { | 
					
						
							|  |  |  | 					return false | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} else if mapKeyStr == "try_policy" || mapKeyStr == "root" { | 
					
						
							|  |  |  | 				if !(isCELStringExpr(mapVal)) { | 
					
						
							|  |  |  | 					return false | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} else { | 
					
						
							|  |  |  | 				return false | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return true | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	case ast.UnspecifiedExprKind, ast.CallKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.SelectKind, ast.StructKind: | 
					
						
							|  |  |  | 		// appeasing the linter :) | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	return false | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // isCELStringExpr indicates whether the expression is a supported string expression | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | func isCELStringExpr(e ast.Expr) bool { | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 	return isCELStringLiteral(e) || isCELCaddyPlaceholderCall(e) || isCELConcatCall(e) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // isCELStringLiteral returns whether the expression is a CEL string literal. | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | func isCELStringLiteral(e ast.Expr) bool { | 
					
						
							|  |  |  | 	switch e.Kind() { | 
					
						
							|  |  |  | 	case ast.LiteralKind: | 
					
						
							|  |  |  | 		constant := e.AsLiteral() | 
					
						
							|  |  |  | 		switch constant.Type() { | 
					
						
							|  |  |  | 		case types.StringType: | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 			return true | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 	case ast.UnspecifiedExprKind, ast.CallKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.MapKind, ast.SelectKind, ast.StructKind: | 
					
						
							|  |  |  | 		// appeasing the linter :) | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	return false | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // isCELCaddyPlaceholderCall returns whether the expression is a caddy placeholder call. | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | func isCELCaddyPlaceholderCall(e ast.Expr) bool { | 
					
						
							|  |  |  | 	switch e.Kind() { | 
					
						
							|  |  |  | 	case ast.CallKind: | 
					
						
							|  |  |  | 		call := e.AsCall() | 
					
						
							|  |  |  | 		if call.FunctionName() == "caddyPlaceholder" { | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 			return true | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 	case ast.UnspecifiedExprKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind: | 
					
						
							|  |  |  | 		// appeasing the linter :) | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	return false | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // isCELConcatCall tests whether the expression is a concat function (+) with string, placeholder, or | 
					
						
							|  |  |  | // other concat call arguments. | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | func isCELConcatCall(e ast.Expr) bool { | 
					
						
							|  |  |  | 	switch e.Kind() { | 
					
						
							|  |  |  | 	case ast.CallKind: | 
					
						
							|  |  |  | 		call := e.AsCall() | 
					
						
							|  |  |  | 		if call.Target().Kind() != ast.UnspecifiedExprKind { | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 			return false | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 		if call.FunctionName() != operators.Add { | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 			return false | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 		for _, arg := range call.Args() { | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 			if !isCELStringExpr(arg) { | 
					
						
							|  |  |  | 				return false | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return true | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 	case ast.UnspecifiedExprKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind: | 
					
						
							|  |  |  | 		// appeasing the linter :) | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	return false | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // isCELStringListLiteral returns whether the expression resolves to a list literal | 
					
						
							|  |  |  | // containing only string constants or a placeholder call. | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | func isCELStringListLiteral(e ast.Expr) bool { | 
					
						
							|  |  |  | 	switch e.Kind() { | 
					
						
							|  |  |  | 	case ast.ListKind: | 
					
						
							|  |  |  | 		list := e.AsList() | 
					
						
							|  |  |  | 		for _, elem := range list.Elements() { | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 			if !isCELStringExpr(elem) { | 
					
						
							|  |  |  | 				return false | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return true | 
					
						
							| 
									
										
										
										
											2024-03-13 23:32:42 -04:00
										 |  |  | 	case ast.UnspecifiedExprKind, ast.CallKind, ast.ComprehensionKind, ast.IdentKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind: | 
					
						
							|  |  |  | 		// appeasing the linter :) | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	return false | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-05 13:53:41 -06:00
										 |  |  | // globSafeRepl replaces special glob characters with escaped | 
					
						
							|  |  |  | // equivalents. Note that the filepath godoc states that | 
					
						
							|  |  |  | // escaping is not done on Windows because of the separator. | 
					
						
							|  |  |  | var globSafeRepl = strings.NewReplacer( | 
					
						
							|  |  |  | 	"*", "\\*", | 
					
						
							|  |  |  | 	"[", "\\[", | 
					
						
							|  |  |  | 	"?", "\\?", | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | const ( | 
					
						
							| 
									
										
										
										
											2019-12-23 12:45:35 -07:00
										 |  |  | 	tryPolicyFirstExist      = "first_exist" | 
					
						
							|  |  |  | 	tryPolicyLargestSize     = "largest_size" | 
					
						
							|  |  |  | 	tryPolicySmallestSize    = "smallest_size" | 
					
						
							|  |  |  | 	tryPolicyMostRecentlyMod = "most_recently_modified" | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Interface guards | 
					
						
							|  |  |  | var ( | 
					
						
							| 
									
										
										
										
											2022-06-22 15:53:46 -07:00
										 |  |  | 	_ caddy.Validator              = (*MatchFile)(nil) | 
					
						
							|  |  |  | 	_ caddyhttp.RequestMatcher     = (*MatchFile)(nil) | 
					
						
							|  |  |  | 	_ caddyhttp.CELLibraryProducer = (*MatchFile)(nil) | 
					
						
							| 
									
										
										
										
											2019-07-11 17:02:57 -06:00
										 |  |  | ) |