From eb8625f7744ba5e72b51549adc086e45313267cb Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 11 Jul 2019 17:02:57 -0600 Subject: Add error & subroute handlers; weakString; other minor handler changes --- modules/caddyhttp/fileserver/matcher.go | 177 ++++++++++++++++++++++------ modules/caddyhttp/fileserver/staticfiles.go | 164 ++------------------------ 2 files changed, 152 insertions(+), 189 deletions(-) (limited to 'modules/caddyhttp/fileserver') diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go index da36eaa..023a69b 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) +) diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index a7c72c9..761dfc3 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -42,27 +42,15 @@ func init() { // FileServer implements a static file server responder for Caddy. type FileServer struct { - Root string `json:"root,omitempty"` // default is current directory - Hide []string `json:"hide,omitempty"` - IndexNames []string `json:"index_names,omitempty"` - Files []string `json:"files,omitempty"` // all relative to the root; default is request URI path - SelectionPolicy string `json:"selection_policy,omitempty"` - Rehandle bool `json:"rehandle,omitempty"` // issue a rehandle (internal redirect) if request is rewritten - Fallback caddyhttp.RouteList `json:"fallback,omitempty"` - Browse *Browse `json:"browse,omitempty"` - // TODO: Etag + Root string `json:"root,omitempty"` // default is current directory + Hide []string `json:"hide,omitempty"` + IndexNames []string `json:"index_names,omitempty"` + Browse *Browse `json:"browse,omitempty"` // TODO: Content negotiation } // Provision sets up the static files responder. func (fsrv *FileServer) Provision(ctx caddy.Context) error { - if fsrv.Fallback != nil { - err := fsrv.Fallback.Provision(ctx) - if err != nil { - return fmt.Errorf("setting up fallback routes: %v", err) - } - } - if fsrv.IndexNames == nil { fsrv.IndexNames = defaultIndexNames } @@ -87,50 +75,14 @@ func (fsrv *FileServer) Provision(ctx caddy.Context) error { return nil } -const ( - selectionPolicyFirstExisting = "first_existing" - selectionPolicyLargestSize = "largest_size" - selectionPolicySmallestSize = "smallest_size" - selectionPolicyRecentlyMod = "most_recently_modified" -) - -// Validate ensures that sf has a valid configuration. -func (fsrv *FileServer) Validate() error { - switch fsrv.SelectionPolicy { - case "", - selectionPolicyFirstExisting, - selectionPolicyLargestSize, - selectionPolicySmallestSize, - selectionPolicyRecentlyMod: - default: - return fmt.Errorf("unknown selection policy %s", fsrv.SelectionPolicy) - } - return nil -} - func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error { repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) filesToHide := fsrv.transformHidePaths(repl) - // map the request to a filename - pathBefore := r.URL.Path - filename := fsrv.selectFile(r, repl, filesToHide) - if filename == "" { - // no files worked, so resort to fallback - if fsrv.Fallback != nil { - fallback := fsrv.Fallback.BuildCompositeRoute(w, r) - return fallback.ServeHTTP(w, r) - } - return caddyhttp.Error(http.StatusNotFound, nil) - } - - // if the ultimate destination has changed, submit - // this request for a rehandling (internal redirect) - // if configured to do so - if r.URL.Path != pathBefore && fsrv.Rehandle { - return caddyhttp.ErrRehandle - } + root := repl.ReplaceAll(fsrv.Root, "") + suffix := repl.ReplaceAll(r.URL.Path, "") + filename := sanitizedPathJoin(root, suffix) // get information about the file info, err := os.Stat(filename) @@ -161,12 +113,8 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, _ cadd } // we found an index file that might work, - // so rewrite the request path and, if - // configured, do an internal redirect + // so rewrite the request path r.URL.Path = path.Join(r.URL.Path, indexPage) - if fsrv.Rehandle { - return caddyhttp.ErrRehandle - } info = indexInfo filename = indexPath @@ -308,107 +256,12 @@ func sanitizedPathJoin(root, reqPath string) string { return filepath.Join(root, filepath.FromSlash(path.Clean("/"+reqPath))) } -// selectFile uses the specified selection policy (or first_existing -// by default) to map the request r to a filename. The full path to -// the file is returned if one is found; otherwise, an empty string -// is returned. -func (fsrv *FileServer) selectFile(r *http.Request, repl caddy.Replacer, filesToHide []string) string { - root := repl.ReplaceAll(fsrv.Root, "") - - if fsrv.Files == nil { - return sanitizedPathJoin(root, r.URL.Path) - } - - switch fsrv.SelectionPolicy { - case "", selectionPolicyFirstExisting: - filesToHide := fsrv.transformHidePaths(repl) - for _, f := range fsrv.Files { - suffix := repl.ReplaceAll(f, "") - fullpath := sanitizedPathJoin(root, suffix) - if !fileHidden(fullpath, filesToHide) && fileExists(fullpath) { - r.URL.Path = suffix - return fullpath - } - } - - case selectionPolicyLargestSize: - var largestSize int64 - var largestFilename string - var largestSuffix string - for _, f := range fsrv.Files { - suffix := repl.ReplaceAll(f, "") - fullpath := sanitizedPathJoin(root, suffix) - if fileHidden(fullpath, filesToHide) { - continue - } - info, err := os.Stat(fullpath) - if err == nil && info.Size() > largestSize { - largestSize = info.Size() - largestFilename = fullpath - largestSuffix = suffix - } - } - r.URL.Path = largestSuffix - return largestFilename - - case selectionPolicySmallestSize: - var smallestSize int64 - var smallestFilename string - var smallestSuffix string - for _, f := range fsrv.Files { - suffix := repl.ReplaceAll(f, "") - fullpath := sanitizedPathJoin(root, suffix) - if fileHidden(fullpath, filesToHide) { - continue - } - info, err := os.Stat(fullpath) - if err == nil && (smallestSize == 0 || info.Size() < smallestSize) { - smallestSize = info.Size() - smallestFilename = fullpath - smallestSuffix = suffix - } - } - r.URL.Path = smallestSuffix - return smallestFilename - - case selectionPolicyRecentlyMod: - var recentDate time.Time - var recentFilename string - var recentSuffix string - for _, f := range fsrv.Files { - suffix := repl.ReplaceAll(f, "") - fullpath := sanitizedPathJoin(root, suffix) - if fileHidden(fullpath, filesToHide) { - continue - } - info, err := os.Stat(fullpath) - if err == nil && - (recentDate.IsZero() || info.ModTime().After(recentDate)) { - recentDate = info.ModTime() - recentFilename = fullpath - recentSuffix = suffix - } - } - r.URL.Path = recentSuffix - return recentFilename - } - - return "" -} - -// fileExists returns true if file exists. -func fileExists(file string) bool { - _, err := os.Stat(file) - return !os.IsNotExist(err) -} - // fileHidden returns true if filename is hidden // according to the hide list. func fileHidden(filename string, hide []string) bool { nameOnly := filepath.Base(filename) sep := string(filepath.Separator) - // see if file is hidden for _, h := range hide { // assuming h is a glob/shell-like pattern, // use it to compare the whole file path; @@ -453,6 +306,5 @@ const minBackoff, maxBackoff = 2, 5 // Interface guards var ( _ caddy.Provisioner = (*FileServer)(nil) - _ caddy.Validator = (*FileServer)(nil) _ caddyhttp.MiddlewareHandler = (*FileServer)(nil) ) -- cgit v1.2.3