diff options
Diffstat (limited to 'modules/caddyhttp/fileserver/staticfiles.go')
-rw-r--r-- | modules/caddyhttp/fileserver/staticfiles.go | 64 |
1 files changed, 51 insertions, 13 deletions
diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index c0fde66..0ed558e 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -29,17 +29,15 @@ import ( "runtime" "strconv" "strings" - "time" + + "go.uber.org/zap" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" - "go.uber.org/zap" ) func init() { - weakrand.Seed(time.Now().UnixNano()) - caddy.RegisterModule(FileServer{}) } @@ -62,7 +60,23 @@ func init() { // requested directory does not have an index file, Caddy writes a // 404 response. Alternatively, file browsing can be enabled with // the "browse" parameter which shows a list of files when directories -// are requested if no index file is present. +// are requested if no index file is present. If "browse" is enabled, +// Caddy may serve a JSON array of the dirctory listing when the `Accept` +// header mentions `application/json` with the following structure: +// +// [{ +// "name": "", +// "size": 0, +// "url": "", +// "mod_time": "", +// "mode": 0, +// "is_dir": false, +// "is_symlink": false +// }] +// +// with the `url` being relative to the request path and `mod_time` in the RFC 3339 format +// with sub-second precision. For any other value for the `Accept` header, the +// respective browse template is executed with `Content-Type: text/html`. // // By default, this handler will canonicalize URIs so that requests to // directories end with a slash, but requests to regular files do not. @@ -250,7 +264,8 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c root := repl.ReplaceAll(fsrv.Root, ".") - filename := caddyhttp.SanitizedPathJoin(root, r.URL.Path) + // remove any trailing `/` as it breaks fs.ValidPath() in the stdlib + filename := strings.TrimSuffix(caddyhttp.SanitizedPathJoin(root, r.URL.Path), "/") fsrv.logger.Debug("sanitized path join", zap.String("site_root", root), @@ -355,7 +370,9 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c } var file fs.File - var etag string + + // etag is usually unset, but if the user knows what they're doing, let them override it + etag := w.Header().Get("Etag") // check for precompressed files for _, ae := range encode.AcceptedEncodings(r, fsrv.PrecompressedOrder) { @@ -387,7 +404,9 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c // don't assign info = compressedInfo because sidecars are kind // of transparent; however we do need to set the Etag: // https://caddy.community/t/gzipped-sidecar-file-wrong-same-etag/16793 - etag = calculateEtag(compressedInfo) + if etag == "" { + etag = calculateEtag(compressedInfo) + } break } @@ -407,20 +426,29 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c } defer file.Close() - etag = calculateEtag(info) + if etag == "" { + etag = calculateEtag(info) + } } // at this point, we're serving a file; Go std lib supports only // GET and HEAD, which is sensible for a static file server - reject // any other methods (see issue #5166) if r.Method != http.MethodGet && r.Method != http.MethodHead { - w.Header().Add("Allow", "GET, HEAD") - return caddyhttp.Error(http.StatusMethodNotAllowed, nil) + // if we're in an error context, then it doesn't make sense + // to repeat the error; just continue because we're probably + // trying to write an error page response (see issue #5703) + if _, ok := r.Context().Value(caddyhttp.ErrorCtxKey).(error); !ok { + w.Header().Add("Allow", "GET, HEAD") + return caddyhttp.Error(http.StatusMethodNotAllowed, nil) + } } // set the Etag - note that a conditional If-None-Match request is handled // by http.ServeContent below, which checks against this Etag value - w.Header().Set("Etag", etag) + if etag != "" { + w.Header().Set("Etag", etag) + } if w.Header().Get("Content-Type") == "" { mtyp := mime.TypeByExtension(filepath.Ext(filename)) @@ -607,7 +635,11 @@ func (fsrv *FileServer) notFound(w http.ResponseWriter, r *http.Request, next ca // Prefix the etag with "W/" to convert it into a weak etag. // See: https://tools.ietf.org/html/rfc7232#section-2.3 func calculateEtag(d os.FileInfo) string { - t := strconv.FormatInt(d.ModTime().Unix(), 36) + mtime := d.ModTime().Unix() + if mtime == 0 || mtime == 1 { + return "" // not useful anyway; see issue #5548 + } + t := strconv.FormatInt(mtime, 36) s := strconv.FormatInt(d.Size(), 36) return `"` + t + s + `"` } @@ -635,6 +667,12 @@ func (wr statusOverrideResponseWriter) WriteHeader(int) { wr.ResponseWriter.WriteHeader(wr.code) } +// Unwrap returns the underlying ResponseWriter, necessary for +// http.ResponseController to work correctly. +func (wr statusOverrideResponseWriter) Unwrap() http.ResponseWriter { + return wr.ResponseWriter +} + // osFS is a simple fs.FS implementation that uses the local // file system. (We do not use os.DirFS because we do our own // rooting or path prefixing without being constrained to a single |