diff options
Diffstat (limited to 'modules/caddyhttp/staticfiles/staticfiles.go')
-rw-r--r-- | modules/caddyhttp/staticfiles/staticfiles.go | 90 |
1 files changed, 63 insertions, 27 deletions
diff --git a/modules/caddyhttp/staticfiles/staticfiles.go b/modules/caddyhttp/staticfiles/staticfiles.go index 0ef3c63..e3af352 100644 --- a/modules/caddyhttp/staticfiles/staticfiles.go +++ b/modules/caddyhttp/staticfiles/staticfiles.go @@ -2,6 +2,7 @@ package staticfiles import ( "fmt" + "html/template" weakrand "math/rand" "net/http" "os" @@ -16,6 +17,8 @@ import ( ) func init() { + weakrand.Seed(time.Now().UnixNano()) + caddy2.RegisterModule(caddy2.Module{ Name: "http.responders.static_files", New: func() (interface{}, error) { return new(StaticFiles), nil }, @@ -25,13 +28,13 @@ func init() { // 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 + 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"` - Hide []string `json:"hide"` - Rehandle bool `json:"rehandle"` // issue a rehandle (internal redirect) if request is rewritten // TODO: Etag // TODO: Content negotiation } @@ -44,9 +47,28 @@ func (sf *StaticFiles) Provision(ctx caddy2.Context) error { 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 } @@ -67,10 +89,6 @@ func (sf *StaticFiles) Validate() error { func (sf *StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error { // TODO: Prevent directory traversal, see https://play.golang.org/p/oh77BiVQFti - // http.FileServer(http.Dir(sf.Directory)).ServeHTTP(w, r) - - ////////////// - // TODO: Still needed? // // Prevent absolute path access on Windows, e.g.: http://localhost:5000/C:\Windows\notepad.exe // // TODO: does stdlib http.Dir handle this? see first check of http.Dir.Open()... @@ -119,7 +137,7 @@ func (sf *StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error { for _, indexPage := range sf.IndexNames { indexPath := path.Join(filename, indexPage) - if fileIsHidden(indexPath, filesToHide) { + if fileHidden(indexPath, filesToHide) { // pretend this file doesn't exist continue } @@ -140,6 +158,7 @@ func (sf *StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error { } info = indexInfo + filename = indexPath break } } @@ -148,43 +167,54 @@ func (sf *StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error { // to browse or return an error if info.IsDir() { if sf.Browse != nil { - return sf.Browse.ServeHTTP(w, r) + return sf.serveBrowse(filename, w, r) } return caddyhttp.Error(http.StatusNotFound, nil) } // open the file - file, err := os.Open(info.Name()) + file, err := sf.openFile(filename, w) if err != nil { - if os.IsNotExist(err) { - return caddyhttp.Error(http.StatusNotFound, err) - } else if os.IsPermission(err) { - return 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 caddyhttp.Error(http.StatusServiceUnavailable, err) + return err } defer file.Close() - // TODO: Right now we return an invalid response if the - // request is for a directory and there is no index file - // or dir browsing; we should return a 404 I think... - // TODO: Etag? // TODO: content negotiation? (brotli sidecar files, etc...) // 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) + // 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 { + 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 +} + +// 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 { @@ -193,6 +223,10 @@ func (sf *StaticFiles) transformHidePaths(repl caddy2.Replacer) []string { return hide } +// 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 root == "" { @@ -211,7 +245,7 @@ func (sf *StaticFiles) selectFile(r *http.Request, repl caddy2.Replacer) string suffix := repl.ReplaceAll(f, "") // TODO: sanitize path fullpath := filepath.Join(root, suffix) - if !fileIsHidden(fullpath, filesToHide) && fileExists(fullpath) { + if !fileHidden(fullpath, filesToHide) && fileExists(fullpath) { r.URL.Path = suffix return fullpath } @@ -282,7 +316,9 @@ func fileExists(file string) bool { return !os.IsNotExist(err) } -func fileIsHidden(filename string, hide []string) bool { +// 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) |