aboutsummaryrefslogtreecommitdiffhomepage
path: root/modules/caddyhttp/fileserver/matcher.go
diff options
context:
space:
mode:
Diffstat (limited to 'modules/caddyhttp/fileserver/matcher.go')
-rw-r--r--modules/caddyhttp/fileserver/matcher.go177
1 files changed, 144 insertions, 33 deletions
diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go
index da36eaa9d..023a69b8f 100644
--- a/modules/caddyhttp/fileserver/matcher.go
+++ b/modules/caddyhttp/fileserver/matcher.go
@@ -15,55 +15,166 @@
package fileserver
import (
+ "fmt"
"net/http"
"os"
+ "time"
+ "github.com/caddyserver/caddy/modules/caddyhttp"
"github.com/caddyserver/caddy/v2"
- "github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func init() {
caddy.RegisterModule(caddy.Module{
Name: "http.matchers.file",
- New: func() interface{} { return new(FileMatcher) },
+ New: func() interface{} { return new(MatchFile) },
})
}
-// FileMatcher is a matcher that can match requests
-// based on the local file system.
-// TODO: Not sure how to do this well; we'd need the ability to
-// hide files, etc...
-// TODO: Also consider a feature to match directory that
-// contains a certain filename (use filepath.Glob), useful
-// if wanting to map directory-URI requests where the dir
-// has index.php to PHP backends, for example (although this
-// can effectively be done with rehandling already)
-type FileMatcher struct {
- Root string `json:"root"`
- Path string `json:"path"`
- Flags []string `json:"flags"`
+// MatchFile is an HTTP request matcher that can match
+// requests based upon file existence.
+type MatchFile struct {
+ // The root directory, used for creating absolute
+ // file paths, and required when working with
+ // relative paths; if not specified, the current
+ // directory is assumed. Accepts placeholders.
+ Root string `json:"root,omitempty"`
+
+ // The list of files to try. Each path here is
+ // considered relatice to Root. If nil, the
+ // request URL's path will be assumed. Accepts
+ // placeholders.
+ TryFiles []string `json:"try_files,omitempty"`
+
+ // How to choose a file in TryFiles.
+ // Default is first_exist.
+ TryPolicy string `json:"try_policy,omitempty"`
+}
+
+// Validate ensures m has a valid configuration.
+func (m MatchFile) Validate() error {
+ switch m.TryPolicy {
+ case "",
+ tryPolicyFirstExist,
+ tryPolicyLargestSize,
+ tryPolicySmallestSize,
+ tryPolicyMostRecentMod:
+ default:
+ return fmt.Errorf("unknown try policy %s", m.TryPolicy)
+ }
+ return nil
+}
+
+// Match returns true if r matches m. Returns true
+// if a file was matched. If so, two placeholders
+// will be available:
+// - http.matchers.file.relative
+// - http.matchers.file.absolute
+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 true
+ }
+ return false
}
-// Match matches the request r against m.
-func (m FileMatcher) Match(r *http.Request) bool {
- fullPath := sanitizedPathJoin(m.Root, m.Path)
- var match bool
- if len(m.Flags) > 0 {
- match = true
- fi, err := os.Stat(fullPath)
- for _, f := range m.Flags {
- switch f {
- case "EXIST":
- match = match && os.IsNotExist(err)
- case "DIR":
- match = match && err == nil && fi.IsDir()
- default:
- match = false
+// 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) {
+ repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
+
+ root := repl.ReplaceAll(m.Root, "")
+
+ // if list of files to try was omitted entirely,
+ // assume URL path
+ if m.TryFiles == nil {
+ // m is not a pointer, so this is safe
+ m.TryFiles = []string{r.URL.Path}
+ }
+
+ switch m.TryPolicy {
+ case "", tryPolicyFirstExist:
+ for _, f := range m.TryFiles {
+ suffix := repl.ReplaceAll(f, "")
+ fullpath := sanitizedPathJoin(root, suffix)
+ if fileExists(fullpath) {
+ return suffix, fullpath, true
}
}
+
+ case tryPolicyLargestSize:
+ var largestSize int64
+ var largestFilename string
+ var largestSuffix string
+ for _, f := range m.TryFiles {
+ suffix := repl.ReplaceAll(f, "")
+ fullpath := sanitizedPathJoin(root, suffix)
+ info, err := os.Stat(fullpath)
+ if err == nil && info.Size() > largestSize {
+ largestSize = info.Size()
+ largestFilename = fullpath
+ largestSuffix = suffix
+ }
+ }
+ return largestSuffix, largestFilename, true
+
+ case tryPolicySmallestSize:
+ var smallestSize int64
+ var smallestFilename string
+ var smallestSuffix string
+ for _, f := range m.TryFiles {
+ suffix := repl.ReplaceAll(f, "")
+ fullpath := sanitizedPathJoin(root, suffix)
+ info, err := os.Stat(fullpath)
+ if err == nil && (smallestSize == 0 || info.Size() < smallestSize) {
+ smallestSize = info.Size()
+ smallestFilename = fullpath
+ smallestSuffix = suffix
+ }
+ }
+ return smallestSuffix, smallestFilename, true
+
+ case tryPolicyMostRecentMod:
+ var recentDate time.Time
+ var recentFilename string
+ var recentSuffix string
+ for _, f := range m.TryFiles {
+ suffix := repl.ReplaceAll(f, "")
+ fullpath := sanitizedPathJoin(root, suffix)
+ info, err := os.Stat(fullpath)
+ if err == nil &&
+ (recentDate.IsZero() || info.ModTime().After(recentDate)) {
+ recentDate = info.ModTime()
+ recentFilename = fullpath
+ recentSuffix = suffix
+ }
+ }
+ return recentSuffix, recentFilename, true
}
- return match
+
+ return
+}
+
+// fileExists returns true if file exists.
+func fileExists(file string) bool {
+ _, err := os.Stat(file)
+ return !os.IsNotExist(err)
}
-// Interface guard
-var _ caddyhttp.RequestMatcher = (*FileMatcher)(nil)
+const (
+ tryPolicyFirstExist = "first_exist"
+ tryPolicyLargestSize = "largest_size"
+ tryPolicySmallestSize = "smallest_size"
+ tryPolicyMostRecentMod = "most_recent_modified"
+)
+
+// Interface guards
+var (
+ _ caddy.Validator = (*MatchFile)(nil)
+ _ caddyhttp.RequestMatcher = (*MatchFile)(nil)
+)