+ {{.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 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}}
+
+
+
+
+
+`
diff --git a/modules/caddyhttp/staticfiles/staticfiles.go b/modules/caddyhttp/staticfiles/staticfiles.go
index 0ef3c63f..e3af3528 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)