From b95b87381a282e1fe57295d145b71645d7801f07 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Wed, 16 Sep 2020 20:09:28 -0400 Subject: fileserver: Fix try_files for directories; windows fix (#3684) * fileserver: Fix try_files for directories, windows fix * fileserver: Add new file type placeholder, refactoring, tests * fileserver: Review cleanup * fileserver: Flip the return args order --- modules/caddyhttp/fileserver/matcher.go | 85 ++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 32 deletions(-) (limited to 'modules/caddyhttp/fileserver/matcher.go') diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go index 1844421..475a9ee 100644 --- a/modules/caddyhttp/fileserver/matcher.go +++ b/modules/caddyhttp/fileserver/matcher.go @@ -19,6 +19,7 @@ import ( "net/http" "os" "path" + "path/filepath" "strings" "time" @@ -34,7 +35,7 @@ func init() { // MatchFile is an HTTP request matcher that can match // requests based upon file existence. // -// Upon matching, two new placeholders will be made +// Upon matching, three new placeholders will be made // available: // // - `{http.matchers.file.relative}` The root-relative @@ -42,6 +43,8 @@ func init() { // requests. // - `{http.matchers.file.absolute}` The absolute path // of the matched file. +// - `{http.matchers.file.type}` Set to "directory" if +// the matched file is a directory, "file" otherwise. type MatchFile struct { // The root directory, used for creating absolute // file paths, and required when working with @@ -153,25 +156,18 @@ func (m MatchFile) Validate() error { } // Match returns true if r matches m. Returns true -// if a file was matched. If so, two placeholders +// if a file was matched. If so, three placeholders // will be available: // - http.matchers.file.relative // - http.matchers.file.absolute +// - http.matchers.file.type func (m MatchFile) Match(r *http.Request) bool { - repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) - rel, abs, matched := m.selectFile(r) - if matched { - repl.Set("http.matchers.file.relative", rel) - repl.Set("http.matchers.file.absolute", abs) - } - return matched + return m.selectFile(r) } // selectFile chooses a file according to m.TryPolicy by appending // the paths in m.TryFiles to m.Root, with placeholder replacements. -// It returns the root-relative path to the matched file, the full -// or absolute path, and whether a match was made. -func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) { +func (m MatchFile) selectFile(r *http.Request) (matched bool) { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) root := repl.ReplaceAll(m.Root, ".") @@ -183,13 +179,35 @@ func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) { m.TryFiles = []string{r.URL.Path} } + // common preparation of the file into parts + prepareFilePath := func(file string) (string, string) { + suffix := m.firstSplit(path.Clean(repl.ReplaceAll(file, ""))) + if strings.HasSuffix(file, "/") { + suffix += "/" + } + fullpath := sanitizedPathJoin(root, suffix) + return suffix, fullpath + } + + // sets up the placeholders for the matched file + setPlaceholders := func(info os.FileInfo, rel string, abs string) { + repl.Set("http.matchers.file.relative", rel) + repl.Set("http.matchers.file.absolute", abs) + + fileType := "file" + if info.IsDir() { + fileType = "directory" + } + repl.Set("http.matchers.file.type", fileType) + } + switch m.TryPolicy { case "", tryPolicyFirstExist: for _, f := range m.TryFiles { - suffix := m.firstSplit(path.Clean(repl.ReplaceAll(f, ""))) - fullpath := sanitizedPathJoin(root, suffix) - if strictFileExists(fullpath) { - return suffix, fullpath, true + suffix, fullpath := prepareFilePath(f) + if info, exists := strictFileExists(fullpath); exists { + setPlaceholders(info, suffix, fullpath) + return true } } @@ -197,9 +215,9 @@ func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) { var largestSize int64 var largestFilename string var largestSuffix string + var info os.FileInfo for _, f := range m.TryFiles { - suffix := m.firstSplit(path.Clean(repl.ReplaceAll(f, ""))) - fullpath := sanitizedPathJoin(root, suffix) + suffix, fullpath := prepareFilePath(f) info, err := os.Stat(fullpath) if err == nil && info.Size() > largestSize { largestSize = info.Size() @@ -207,15 +225,16 @@ func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) { largestSuffix = suffix } } - return largestSuffix, largestFilename, true + setPlaceholders(info, largestSuffix, largestFilename) + return true case tryPolicySmallestSize: var smallestSize int64 var smallestFilename string var smallestSuffix string + var info os.FileInfo for _, f := range m.TryFiles { - suffix := m.firstSplit(path.Clean(repl.ReplaceAll(f, ""))) - fullpath := sanitizedPathJoin(root, suffix) + suffix, fullpath := prepareFilePath(f) info, err := os.Stat(fullpath) if err == nil && (smallestSize == 0 || info.Size() < smallestSize) { smallestSize = info.Size() @@ -223,15 +242,16 @@ func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) { smallestSuffix = suffix } } - return smallestSuffix, smallestFilename, true + setPlaceholders(info, smallestSuffix, smallestFilename) + return true case tryPolicyMostRecentlyMod: var recentDate time.Time var recentFilename string var recentSuffix string + var info os.FileInfo for _, f := range m.TryFiles { - suffix := m.firstSplit(path.Clean(repl.ReplaceAll(f, ""))) - fullpath := sanitizedPathJoin(root, suffix) + suffix, fullpath := prepareFilePath(f) info, err := os.Stat(fullpath) if err == nil && (recentDate.IsZero() || info.ModTime().After(recentDate)) { @@ -240,7 +260,8 @@ func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) { recentSuffix = suffix } } - return recentSuffix, recentFilename, true + setPlaceholders(info, recentSuffix, recentFilename) + return true } return @@ -252,7 +273,7 @@ func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) { // the file must also be a directory; if it does // NOT end in a forward slash, the file must NOT // be a directory. -func strictFileExists(file string) bool { +func strictFileExists(file string) (os.FileInfo, bool) { stat, err := os.Stat(file) if err != nil { // in reality, this can be any error @@ -263,16 +284,16 @@ func strictFileExists(file string) bool { // the file exists, so we just treat any // error as if it does not exist; see // https://stackoverflow.com/a/12518877/1048862 - return false + return nil, false } - if strings.HasSuffix(file, "/") { + if strings.HasSuffix(file, string(filepath.Separator)) { // by convention, file paths ending - // in a slash must be a directory - return stat.IsDir() + // in a path separator must be a directory + return stat, stat.IsDir() } // by convention, file paths NOT ending - // in a slash must NOT be a directory - return !stat.IsDir() + // in a path separator must NOT be a directory + return stat, !stat.IsDir() } // firstSplit returns the first result where the path -- cgit v1.2.3