summaryrefslogtreecommitdiff
path: root/modules/caddyhttp/staticfiles/staticfiles.go
diff options
context:
space:
mode:
authorMatthew Holt <mholt@users.noreply.github.com>2019-05-20 21:21:33 -0600
committerMatthew Holt <mholt@users.noreply.github.com>2019-05-20 21:21:33 -0600
commita9698728506c580bc38db2e122a5e6ef07f85ce6 (patch)
treea583112f114fc01e09867aa123578861637b04d5 /modules/caddyhttp/staticfiles/staticfiles.go
parentaaacab1bc3790e3a207ae5d70ca6559cac265bff (diff)
Default error handler; rename StaticFiles -> FileServer
Diffstat (limited to 'modules/caddyhttp/staticfiles/staticfiles.go')
-rw-r--r--modules/caddyhttp/staticfiles/staticfiles.go398
1 files changed, 0 insertions, 398 deletions
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)