From a9698728506c580bc38db2e122a5e6ef07f85ce6 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 20 May 2019 21:21:33 -0600 Subject: Default error handler; rename StaticFiles -> FileServer --- modules/caddyhttp/staticfiles/browse.go | 157 --------- modules/caddyhttp/staticfiles/browselisting.go | 245 ------------- modules/caddyhttp/staticfiles/browsetpl.go | 403 ---------------------- modules/caddyhttp/staticfiles/matcher.go | 57 --- modules/caddyhttp/staticfiles/staticfiles.go | 398 --------------------- modules/caddyhttp/staticfiles/staticfiles_test.go | 78 ----- 6 files changed, 1338 deletions(-) delete mode 100644 modules/caddyhttp/staticfiles/browse.go delete mode 100644 modules/caddyhttp/staticfiles/browselisting.go delete mode 100644 modules/caddyhttp/staticfiles/browsetpl.go delete mode 100644 modules/caddyhttp/staticfiles/matcher.go delete mode 100644 modules/caddyhttp/staticfiles/staticfiles.go delete mode 100644 modules/caddyhttp/staticfiles/staticfiles_test.go (limited to 'modules/caddyhttp/staticfiles') diff --git a/modules/caddyhttp/staticfiles/browse.go b/modules/caddyhttp/staticfiles/browse.go deleted file mode 100644 index 2bb130f..0000000 --- a/modules/caddyhttp/staticfiles/browse.go +++ /dev/null @@ -1,157 +0,0 @@ -package staticfiles - -import ( - "bytes" - "encoding/json" - "html/template" - "net/http" - "os" - "path" - "strings" - - "bitbucket.org/lightcodelabs/caddy2" - "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp" -) - -// Browse configures directory browsing. -type Browse struct { - TemplateFile string `json:"template_file"` - - template *template.Template -} - -func (sf *StaticFiles) serveBrowse(dirPath string, w http.ResponseWriter, r *http.Request) error { - dir, err := sf.openFile(dirPath, w) - if err != nil { - return err - } - defer dir.Close() - - repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer) - - listing, err := sf.loadDirectoryContents(dir, r.URL.Path, repl) - switch { - case os.IsPermission(err): - return caddyhttp.Error(http.StatusForbidden, err) - case os.IsNotExist(err): - return caddyhttp.Error(http.StatusNotFound, err) - case err != nil: - return caddyhttp.Error(http.StatusInternalServerError, err) - } - - sf.browseApplyQueryParams(w, r, &listing) - - // write response as either JSON or HTML - var buf *bytes.Buffer - acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ",")) - if strings.Contains(acceptHeader, "application/json") { - if buf, err = sf.browseWriteJSON(listing); err != nil { - return caddyhttp.Error(http.StatusInternalServerError, err) - } - w.Header().Set("Content-Type", "application/json; charset=utf-8") - } else { - if buf, err = sf.browseWriteHTML(listing); err != nil { - return caddyhttp.Error(http.StatusInternalServerError, err) - } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - } - buf.WriteTo(w) - - return nil - - // TODO: Sigh... do we have to put this here? - // // Browsing navigation gets messed up if browsing a directory - // // that doesn't end in "/" (which it should, anyway) - // u := *r.URL - // if u.Path == "" { - // u.Path = "/" - // } - // if u.Path[len(u.Path)-1] != '/' { - // u.Path += "/" - // http.Redirect(w, r, u.String(), http.StatusMovedPermanently) - // return http.StatusMovedPermanently, nil - // } - - // return b.ServeListing(w, r, requestedFilepath, bc) -} - -func (sf *StaticFiles) loadDirectoryContents(dir *os.File, urlPath string, repl caddy2.Replacer) (browseListing, error) { - files, err := dir.Readdir(-1) - if err != nil { - return browseListing{}, err - } - - // determine if user can browse up another folder - curPathDir := path.Dir(strings.TrimSuffix(urlPath, "/")) - canGoUp := strings.HasPrefix(curPathDir, sf.Root) - - return sf.directoryListing(files, canGoUp, urlPath, repl), nil -} - -// browseApplyQueryParams applies query parameters to the listing. -// It mutates the listing and may set cookies. -func (sf *StaticFiles) browseApplyQueryParams(w http.ResponseWriter, r *http.Request, listing *browseListing) { - sortParam := r.URL.Query().Get("sort") - orderParam := r.URL.Query().Get("order") - limitParam := r.URL.Query().Get("limit") - - // first figure out what to sort by - switch sortParam { - case "": - sortParam = sortByNameDirFirst - if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil { - sortParam = sortCookie.Value - } - case sortByName, sortByNameDirFirst, sortBySize, sortByTime: - http.SetCookie(w, &http.Cookie{Name: "sort", Value: sortParam, Secure: r.TLS != nil}) - } - - // then figure out the order - switch orderParam { - case "": - orderParam = "asc" - if orderCookie, orderErr := r.Cookie("order"); orderErr == nil { - orderParam = orderCookie.Value - } - case "asc", "desc": - http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil}) - } - - // finally, apply the sorting and limiting - listing.applySortAndLimit(sortParam, orderParam, limitParam) -} - -func (sf *StaticFiles) browseWriteJSON(listing browseListing) (*bytes.Buffer, error) { - buf := new(bytes.Buffer) - err := json.NewEncoder(buf).Encode(listing.Items) - return buf, err -} - -func (sf *StaticFiles) browseWriteHTML(listing browseListing) (*bytes.Buffer, error) { - buf := new(bytes.Buffer) - err := sf.Browse.template.Execute(buf, listing) - return buf, err -} - -// isSymlink return true if f is a symbolic link -func isSymlink(f os.FileInfo) bool { - return f.Mode()&os.ModeSymlink != 0 -} - -// isSymlinkTargetDir return true if f's symbolic link target -// is a directory. Return false if not a symbolic link. -// TODO: Re-implement -func isSymlinkTargetDir(f os.FileInfo, urlPath string) bool { - // if !isSymlink(f) { - // return false - // } - - // // TODO: Ensure path is sanitized - // target:= path.Join(root, urlPath, f.Name())) - // targetInfo, err := os.Stat(target) - // if err != nil { - // return false - // } - // return targetInfo.IsDir() - return false -} diff --git a/modules/caddyhttp/staticfiles/browselisting.go b/modules/caddyhttp/staticfiles/browselisting.go deleted file mode 100644 index 11e6b9c..0000000 --- a/modules/caddyhttp/staticfiles/browselisting.go +++ /dev/null @@ -1,245 +0,0 @@ -package staticfiles - -import ( - "net/url" - "os" - "path" - "sort" - "strconv" - "strings" - "time" - - "bitbucket.org/lightcodelabs/caddy2" - "github.com/dustin/go-humanize" -) - -func (sf *StaticFiles) directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, repl caddy2.Replacer) browseListing { - filesToHide := sf.transformHidePaths(repl) - - var ( - fileInfos []fileInfo - dirCount, fileCount int - ) - - for _, f := range files { - name := f.Name() - - if fileHidden(name, filesToHide) { - continue - } - - isDir := f.IsDir() || isSymlinkTargetDir(f, urlPath) - - if isDir { - name += "/" - dirCount++ - } else { - fileCount++ - } - - u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name - - fileInfos = append(fileInfos, fileInfo{ - IsDir: isDir, - IsSymlink: isSymlink(f), - Name: f.Name(), - Size: f.Size(), - URL: u.String(), - ModTime: f.ModTime().UTC(), - Mode: f.Mode(), - }) - } - - return browseListing{ - Name: path.Base(urlPath), - Path: urlPath, - CanGoUp: canGoUp, - Items: fileInfos, - NumDirs: dirCount, - NumFiles: fileCount, - } -} - -type browseListing struct { - // The name of the directory (the last element of the path). - Name string - - // The full path of the request. - Path string - - // Whether the parent directory is browseable. - CanGoUp bool - - // The items (files and folders) in the path. - Items []fileInfo - - // The number of directories in the listing. - NumDirs int - - // The number of files (items that aren't directories) in the listing. - NumFiles int - - // Sort column used - Sort string - - // Sorting order - Order string - - // If ≠0 then Items have been limited to that many elements. - ItemsLimitedTo int -} - -// Breadcrumbs returns l.Path where every element maps -// the link to the text to display. -func (l browseListing) Breadcrumbs() []crumb { - var result []crumb - - if len(l.Path) == 0 { - return result - } - - // skip trailing slash - lpath := l.Path - if lpath[len(lpath)-1] == '/' { - lpath = lpath[:len(lpath)-1] - } - - parts := strings.Split(lpath, "/") - for i := range parts { - txt := parts[i] - if i == 0 && parts[i] == "" { - txt = "/" - } - lnk := strings.Repeat("../", len(parts)-i-1) - result = append(result, crumb{Link: lnk, Text: txt}) - } - - return result -} - -func (l *browseListing) applySortAndLimit(sortParam, orderParam, limitParam string) { - l.Sort = sortParam - l.Order = orderParam - - if l.Order == "desc" { - switch l.Sort { - case sortByName: - sort.Sort(sort.Reverse(byName(*l))) - case sortByNameDirFirst: - sort.Sort(sort.Reverse(byNameDirFirst(*l))) - case sortBySize: - sort.Sort(sort.Reverse(bySize(*l))) - case sortByTime: - sort.Sort(sort.Reverse(byTime(*l))) - } - } else { - switch l.Sort { - case sortByName: - sort.Sort(byName(*l)) - case sortByNameDirFirst: - sort.Sort(byNameDirFirst(*l)) - case sortBySize: - sort.Sort(bySize(*l)) - case sortByTime: - sort.Sort(byTime(*l)) - } - } - - if limitParam != "" { - limit, _ := strconv.Atoi(limitParam) - if limit > 0 && limit <= len(l.Items) { - l.Items = l.Items[:limit] - l.ItemsLimitedTo = limit - } - } -} - -// crumb represents part of a breadcrumb menu, -// pairing a link with the text to display. -type crumb struct { - Link, Text string -} - -// fileInfo contains serializable information -// about a file or directory. -type fileInfo struct { - Name string `json:"name"` - Size int64 `json:"size"` - URL string `json:"url"` - ModTime time.Time `json:"mod_time"` - Mode os.FileMode `json:"mode"` - IsDir bool `json:"is_dir"` - IsSymlink bool `json:"is_symlink"` -} - -// HumanSize returns the size of the file as a -// human-readable string in IEC format (i.e. -// power of 2 or base 1024). -func (fi fileInfo) HumanSize() string { - return humanize.IBytes(uint64(fi.Size)) -} - -// HumanModTime returns the modified time of the file -// as a human-readable string given by format. -func (fi fileInfo) HumanModTime(format string) string { - return fi.ModTime.Format(format) -} - -type byName browseListing -type byNameDirFirst browseListing -type bySize browseListing -type byTime browseListing - -func (l byName) Len() int { return len(l.Items) } -func (l byName) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] } - -func (l byName) Less(i, j int) bool { - return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name) -} - -func (l byNameDirFirst) Len() int { return len(l.Items) } -func (l byNameDirFirst) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] } - -func (l byNameDirFirst) Less(i, j int) bool { - // sort by name if both are dir or file - if l.Items[i].IsDir == l.Items[j].IsDir { - return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name) - } - // sort dir ahead of file - return l.Items[i].IsDir -} - -func (l bySize) Len() int { return len(l.Items) } -func (l bySize) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] } - -func (l bySize) Less(i, j int) bool { - const directoryOffset = -1 << 31 // = -math.MinInt32 - - iSize, jSize := l.Items[i].Size, l.Items[j].Size - - // directory sizes depend on the file system; to - // provide a consistent experience, put them up front - // and sort them by name - if l.Items[i].IsDir { - iSize = directoryOffset - } - if l.Items[j].IsDir { - jSize = directoryOffset - } - if l.Items[i].IsDir && l.Items[j].IsDir { - return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name) - } - - return iSize < jSize -} - -func (l byTime) Len() int { return len(l.Items) } -func (l byTime) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] } -func (l byTime) Less(i, j int) bool { return l.Items[i].ModTime.Before(l.Items[j].ModTime) } - -const ( - sortByName = "name" - sortByNameDirFirst = "name_dir_first" - sortBySize = "size" - sortByTime = "time" -) diff --git a/modules/caddyhttp/staticfiles/browsetpl.go b/modules/caddyhttp/staticfiles/browsetpl.go deleted file mode 100644 index ff2a1e1..0000000 --- a/modules/caddyhttp/staticfiles/browsetpl.go +++ /dev/null @@ -1,403 +0,0 @@ -package staticfiles - -const defaultBrowseTemplate = ` - - - {{html .Name}} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

- {{range $i, $crumb := .Breadcrumbs}}{{html $crumb.Text}}{{if ne $i 0}}/{{end}}{{end}} -

-
-
-
-
- {{.NumDirs}} director{{if eq 1 .NumDirs}}y{{else}}ies{{end}} - {{.NumFiles}} file{{if ne 1 .NumFiles}}s{{end}} - {{- if ne 0 .ItemsLimitedTo}} - (of which only {{.ItemsLimitedTo}} are displayed) - {{- end}} - -
-
-
- - - - - - - - - - - - {{- if .CanGoUp}} - - - - - - - - {{- end}} - {{- range .Items}} - - - - {{- if .IsDir}} - - {{- else}} - - {{- end}} - - - - {{- end}} - -
- {{- if and (eq .Sort "namedirfirst") (ne .Order "desc")}} - - {{- else if and (eq .Sort "namedirfirst") (ne .Order "asc")}} - - {{- else}} - - {{- end}} - - {{- if and (eq .Sort "name") (ne .Order "desc")}} - Name - {{- else if and (eq .Sort "name") (ne .Order "asc")}} - Name - {{- else}} - Name - {{- end}} - - {{- if and (eq .Sort "size") (ne .Order "desc")}} - Size - {{- else if and (eq .Sort "size") (ne .Order "asc")}} - Size - {{- else}} - Size - {{- end}} - - {{- if and (eq .Sort "time") (ne .Order "desc")}} - Modified - {{- else if and (eq .Sort "time") (ne .Order "asc")}} - Modified - {{- else}} - Modified - {{- end}} -
- - Go up - -
- - {{- if .IsDir}} - - {{- else}} - - {{- end}} - {{html .Name}} - - {{.HumanSize}}
-
-
- - - -` diff --git a/modules/caddyhttp/staticfiles/matcher.go b/modules/caddyhttp/staticfiles/matcher.go deleted file mode 100644 index 9ce3f4c..0000000 --- a/modules/caddyhttp/staticfiles/matcher.go +++ /dev/null @@ -1,57 +0,0 @@ -package staticfiles - -import ( - "net/http" - "os" - "path/filepath" - - "bitbucket.org/lightcodelabs/caddy2" - "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp" -) - -func init() { - caddy2.RegisterModule(caddy2.Module{ - Name: "http.matchers.file", - New: func() (interface{}, error) { return new(FileMatcher), nil }, - }) -} - -// 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"` -} - -// Match matches the request r against m. -func (m FileMatcher) Match(r *http.Request) bool { - // TODO: sanitize path - fullPath := filepath.Join(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 - } - } - } - return match -} - -// Interface guard -var _ caddyhttp.RequestMatcher = (*FileMatcher)(nil) diff --git a/modules/caddyhttp/staticfiles/staticfiles.go b/modules/caddyhttp/staticfiles/staticfiles.go deleted file mode 100644 index f9fd8d2..0000000 --- a/modules/caddyhttp/staticfiles/staticfiles.go +++ /dev/null @@ -1,398 +0,0 @@ -package staticfiles - -import ( - "fmt" - "html/template" - weakrand "math/rand" - "net/http" - "os" - "path" - "path/filepath" - "strconv" - "strings" - "time" - - "bitbucket.org/lightcodelabs/caddy2" - "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp" -) - -func init() { - weakrand.Seed(time.Now().UnixNano()) - - caddy2.RegisterModule(caddy2.Module{ - Name: "http.responders.static_files", - New: func() (interface{}, error) { return new(StaticFiles), nil }, - }) -} - -// StaticFiles implements a static file server responder for Caddy. -type StaticFiles struct { - Root string `json:"root"` // default is current directory - Hide []string `json:"hide"` - IndexNames []string `json:"index_names"` - Files []string `json:"files"` // all relative to the root; default is request URI path - Rehandle bool `json:"rehandle"` // issue a rehandle (internal redirect) if request is rewritten - SelectionPolicy string `json:"selection_policy"` - Fallback caddyhttp.RouteList `json:"fallback"` - Browse *Browse `json:"browse"` - // TODO: Etag - // TODO: Content negotiation -} - -// Provision sets up the static files responder. -func (sf *StaticFiles) Provision(ctx caddy2.Context) error { - if sf.Fallback != nil { - err := sf.Fallback.Provision(ctx) - if err != nil { - return fmt.Errorf("setting up fallback routes: %v", err) - } - } - - if sf.IndexNames == nil { - sf.IndexNames = defaultIndexNames - } - - if sf.Browse != nil { - var tpl *template.Template - var err error - if sf.Browse.TemplateFile != "" { - tpl, err = template.ParseFiles(sf.Browse.TemplateFile) - if err != nil { - return fmt.Errorf("parsing browse template file: %v", err) - } - } else { - tpl, err = template.New("default_listing").Parse(defaultBrowseTemplate) - if err != nil { - return fmt.Errorf("parsing default browse template: %v", err) - } - } - sf.Browse.template = tpl - } - - 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 (sf *StaticFiles) Validate() error { - switch sf.SelectionPolicy { - case "", - selectionPolicyFirstExisting, - selectionPolicyLargestSize, - selectionPolicySmallestSize, - selectionPolicyRecentlyMod: - default: - return fmt.Errorf("unknown selection policy %s", sf.SelectionPolicy) - } - return nil -} - -func (sf *StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error { - repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer) - - // map the request to a filename - pathBefore := r.URL.Path - filename := sf.selectFile(r, repl) - if filename == "" { - // no files worked, so resort to fallback - if sf.Fallback != nil { - fallback := sf.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 && sf.Rehandle { - return caddyhttp.ErrRehandle - } - - // get information about the file - info, err := os.Stat(filename) - if err != nil { - err = mapDirOpenError(err, filename) - if os.IsNotExist(err) { - return caddyhttp.Error(http.StatusNotFound, err) - } else if os.IsPermission(err) { - return caddyhttp.Error(http.StatusForbidden, err) - } - // TODO: treat this as resource exhaustion like with os.Open? Or unnecessary here? - return caddyhttp.Error(http.StatusInternalServerError, err) - } - - // if the request mapped to a directory, see if - // there is an index file we can serve - if info.IsDir() && len(sf.IndexNames) > 0 { - filesToHide := sf.transformHidePaths(repl) - - for _, indexPage := range sf.IndexNames { - indexPath := sanitizedPathJoin(filename, indexPage) - if fileHidden(indexPath, filesToHide) { - // pretend this file doesn't exist - continue - } - - indexInfo, err := os.Stat(indexPath) - if err != nil { - continue - } - - // we found an index file that might work, - // so rewrite the request path and, if - // configured, do an internal redirect - r.URL.Path = path.Join(r.URL.Path, indexPage) - if sf.Rehandle { - return caddyhttp.ErrRehandle - } - - info = indexInfo - filename = indexPath - break - } - } - - // if still referencing a directory, delegate - // to browse or return an error - if info.IsDir() { - if sf.Browse != nil { - return sf.serveBrowse(filename, w, r) - } - return caddyhttp.Error(http.StatusNotFound, nil) - } - - // TODO: content negotiation (brotli sidecar files, etc...) - - // open the file - file, err := sf.openFile(filename, w) - if err != nil { - return err - } - defer file.Close() - - // TODO: Etag - - // let the standard library do what it does best; note, however, - // that errors generated by ServeContent are written immediately - // to the response, so we cannot handle them (but errors here - // are rare) - http.ServeContent(w, r, info.Name(), info.ModTime(), file) - - return nil -} - -// openFile opens the file at the given filename. If there was an error, -// the response is configured to inform the client how to best handle it -// and a well-described handler error is returned (do not wrap the -// returned error value). -func (sf *StaticFiles) openFile(filename string, w http.ResponseWriter) (*os.File, error) { - file, err := os.Open(filename) - if err != nil { - err = mapDirOpenError(err, filename) - if os.IsNotExist(err) { - return nil, caddyhttp.Error(http.StatusNotFound, err) - } else if os.IsPermission(err) { - return nil, caddyhttp.Error(http.StatusForbidden, err) - } - // maybe the server is under load and ran out of file descriptors? - // have client wait arbitrary seconds to help prevent a stampede - backoff := weakrand.Intn(maxBackoff-minBackoff) + minBackoff - w.Header().Set("Retry-After", strconv.Itoa(backoff)) - return nil, caddyhttp.Error(http.StatusServiceUnavailable, err) - } - return file, nil -} - -// mapDirOpenError maps the provided non-nil error from opening name -// to a possibly better non-nil error. In particular, it turns OS-specific errors -// about opening files in non-directories into os.ErrNotExist. See golang/go#18984. -// Adapted from the Go standard library; originally written by Nathaniel Caza. -// https://go-review.googlesource.com/c/go/+/36635/ -// https://go-review.googlesource.com/c/go/+/36804/ -func mapDirOpenError(originalErr error, name string) error { - if os.IsNotExist(originalErr) || os.IsPermission(originalErr) { - return originalErr - } - - parts := strings.Split(name, string(filepath.Separator)) - for i := range parts { - if parts[i] == "" { - continue - } - fi, err := os.Stat(strings.Join(parts[:i+1], string(filepath.Separator))) - if err != nil { - return originalErr - } - if !fi.IsDir() { - return os.ErrNotExist - } - } - - return originalErr -} - -// transformHidePaths performs replacements for all the elements of -// sf.Hide and returns a new list of the transformed values. -func (sf *StaticFiles) transformHidePaths(repl caddy2.Replacer) []string { - hide := make([]string, len(sf.Hide)) - for i := range sf.Hide { - hide[i] = repl.ReplaceAll(sf.Hide[i], "") - } - return hide -} - -// sanitizedPathJoin performs filepath.Join(root, reqPath) that -// is safe against directory traversal attacks. It uses logic -// similar to that in the Go standard library, specifically -// in the implementation of http.Dir. -func sanitizedPathJoin(root, reqPath string) string { - // TODO: Caddy 1 uses this: - // prevent absolute path access on Windows, e.g. http://localhost:5000/C:\Windows\notepad.exe - // if runtime.GOOS == "windows" && len(reqPath) > 0 && filepath.IsAbs(reqPath[1:]) { - // TODO. - // } - - // TODO: whereas std lib's http.Dir.Open() uses this: - // if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) { - // return nil, errors.New("http: invalid character in file path") - // } - - // TODO: see https://play.golang.org/p/oh77BiVQFti for another thing to consider - - if root == "" { - root = "." - } - 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 (sf *StaticFiles) selectFile(r *http.Request, repl caddy2.Replacer) string { - root := repl.ReplaceAll(sf.Root, "") - - if sf.Files == nil { - return sanitizedPathJoin(root, r.URL.Path) - } - - switch sf.SelectionPolicy { - case "", selectionPolicyFirstExisting: - filesToHide := sf.transformHidePaths(repl) - for _, f := range sf.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 sf.Files { - 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 - } - } - r.URL.Path = largestSuffix - return largestFilename - - case selectionPolicySmallestSize: - var smallestSize int64 - var smallestFilename string - var smallestSuffix string - for _, f := range sf.Files { - 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 - } - } - r.URL.Path = smallestSuffix - return smallestFilename - - case selectionPolicyRecentlyMod: - var recentDate time.Time - var recentFilename string - var recentSuffix string - for _, f := range sf.Files { - 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 - } - } - 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; - // but if there is no separator in h, then - // just compare against the file's name - compare := filename - if !strings.Contains(h, sep) { - compare = nameOnly - } - - hidden, err := filepath.Match(h, compare) - if err != nil { - // malformed pattern; fallback by checking prefix - if strings.HasPrefix(filename, h) { - return true - } - } - if hidden { - // file name or path matches hide pattern - return true - } - } - - return false -} - -var defaultIndexNames = []string{"index.html"} - -const minBackoff, maxBackoff = 2, 5 - -// Interface guard -var _ caddyhttp.Handler = (*StaticFiles)(nil) diff --git a/modules/caddyhttp/staticfiles/staticfiles_test.go b/modules/caddyhttp/staticfiles/staticfiles_test.go deleted file mode 100644 index f2e1c89..0000000 --- a/modules/caddyhttp/staticfiles/staticfiles_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package staticfiles - -import ( - "net/url" - "testing" -) - -func TestSanitizedPathJoin(t *testing.T) { - // For easy reference: - // %2E = . - // %2F = / - // %5C = \ - for i, tc := range []struct { - inputRoot string - inputPath string - expect string - }{ - { - inputPath: "", - expect: ".", - }, - { - inputPath: "/", - expect: ".", - }, - { - inputPath: "/foo", - expect: "foo", - }, - { - inputPath: "/foo/bar", - expect: "foo/bar", - }, - { - inputRoot: "/a", - inputPath: "/foo/bar", - expect: "/a/foo/bar", - }, - { - inputPath: "/foo/../bar", - expect: "bar", - }, - { - inputRoot: "/a/b", - inputPath: "/foo/../bar", - expect: "/a/b/bar", - }, - { - inputRoot: "/a/b", - inputPath: "/..%2fbar", - expect: "/a/b/bar", - }, - { - inputRoot: "/a/b", - inputPath: "/%2e%2e%2fbar", - expect: "/a/b/bar", - }, - { - inputRoot: "/a/b", - inputPath: "/%2e%2e%2f%2e%2e%2f", - expect: "/a/b", - }, - } { - // we don't *need* to use an actual parsed URL, but it - // adds some authenticity to the tests since real-world - // values will be coming in from URLs; thus, the test - // corpus can contain paths as encoded by clients, which - // more closely emulates the actual attack vector - u, err := url.Parse("http://test:9999" + tc.inputPath) - if err != nil { - t.Fatalf("Test %d: invalid URL: %v", i, err) - } - actual := sanitizedPathJoin(tc.inputRoot, u.Path) - if actual != tc.expect { - t.Errorf("Test %d: [%s %s] => %s (expected %s)", i, tc.inputRoot, tc.inputPath, actual, tc.expect) - } - } -} -- cgit v1.2.3