2019-06-30 17:07:58 -05:00
|
|
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
2019-05-20 22:21:33 -05:00
|
|
|
package fileserver
|
2019-03-31 21:41:29 -05:00
|
|
|
|
|
|
|
import (
|
2022-07-30 14:07:44 -05:00
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
2019-05-20 11:59:20 -05:00
|
|
|
"fmt"
|
2022-07-30 14:07:44 -05:00
|
|
|
"io"
|
|
|
|
"io/fs"
|
2019-05-20 11:59:20 -05:00
|
|
|
weakrand "math/rand"
|
2019-06-21 15:36:26 -05:00
|
|
|
"mime"
|
2019-03-31 21:41:29 -05:00
|
|
|
"net/http"
|
2019-05-20 11:59:20 -05:00
|
|
|
"os"
|
fileserver: Only redirect if filename not rewritten (fix #4205)
This is the more correct implementation of 23dadc0d86dd75dad7559c25f20c9641bc7bc30f (#4179)... I think. This commit effectively undoes the revert in 8848df9c5d372a559d01512b7a4ef00e38867b55, but with corrections to the logic.
We *do* need to use the original request path (the path the browser knows) for redirects, since they are external, and rewrites are only internal.
However, if the path was rewritten to a non-canonical path, we should not redirect to canonicalize that, since rewrites are intentional by the site owner. Canonicalizing the path involves modifying only the suffix (base element, or filename) of the path. Thus, if a rewrite involves only the prefix (like how handle_path strips a path prefix), then we can (hopefully!) safely redirect using the original URI since the filename was not rewritten.
So basically, if rewrites modify the filename, we should not canonicalize those requests. If rewrites only modify another part of the path (commonly a prefix), we should be OK to redirect.
2021-06-17 10:55:49 -05:00
|
|
|
"path"
|
2019-05-20 11:59:20 -05:00
|
|
|
"path/filepath"
|
2022-10-18 22:55:25 -05:00
|
|
|
"runtime"
|
2019-05-20 11:59:20 -05:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2019-03-31 21:41:29 -05:00
|
|
|
|
2023-08-14 10:41:15 -05:00
|
|
|
"go.uber.org/zap"
|
|
|
|
|
2019-07-02 13:37:06 -05:00
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
2021-03-29 19:47:19 -05:00
|
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
2019-03-31 21:41:29 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
2019-08-21 11:46:35 -05:00
|
|
|
caddy.RegisterModule(FileServer{})
|
2019-03-31 21:41:29 -05:00
|
|
|
}
|
|
|
|
|
2022-07-30 14:07:44 -05:00
|
|
|
// FileServer implements a handler that serves static files.
|
|
|
|
//
|
|
|
|
// The path of the file to serve is constructed by joining the site root
|
|
|
|
// and the sanitized request path. Any and all files within the root and
|
|
|
|
// links with targets outside the site root may therefore be accessed.
|
|
|
|
// For example, with a site root of `/www`, requests to `/foo/bar.txt`
|
|
|
|
// will serve the file at `/www/foo/bar.txt`.
|
|
|
|
//
|
|
|
|
// The request path is sanitized using the Go standard library's
|
|
|
|
// path.Clean() function (https://pkg.go.dev/path#Clean) before being
|
|
|
|
// joined to the root. Request paths must be valid and well-formed.
|
|
|
|
//
|
|
|
|
// For requests that access directories instead of regular files,
|
|
|
|
// Caddy will attempt to serve an index file if present. For example,
|
|
|
|
// a request to `/dir/` will attempt to serve `/dir/index.html` if
|
|
|
|
// it exists. The index file names to try are configurable. If a
|
|
|
|
// 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
|
2023-08-18 14:04:08 -05:00
|
|
|
// 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`.
|
2022-07-30 14:07:44 -05:00
|
|
|
//
|
|
|
|
// By default, this handler will canonicalize URIs so that requests to
|
|
|
|
// directories end with a slash, but requests to regular files do not.
|
|
|
|
// This is enforced with HTTP redirects automatically and can be disabled.
|
|
|
|
// Canonicalization redirects are not issued, however, if a URI rewrite
|
|
|
|
// modified the last component of the path (the filename).
|
|
|
|
//
|
|
|
|
// This handler sets the Etag and Last-Modified headers for static files.
|
|
|
|
// It does not perform MIME sniffing to determine Content-Type based on
|
|
|
|
// contents, but does use the extension (if known); see the Go docs for
|
|
|
|
// details: https://pkg.go.dev/mime#TypeByExtension
|
|
|
|
//
|
|
|
|
// The file server properly handles requests with If-Match,
|
|
|
|
// If-Unmodified-Since, If-Modified-Since, If-None-Match, Range, and
|
|
|
|
// If-Range headers. It includes the file's modification time in the
|
|
|
|
// Last-Modified header of the response.
|
2019-05-20 22:21:33 -05:00
|
|
|
type FileServer struct {
|
2022-07-30 14:07:44 -05:00
|
|
|
// The file system implementation to use. By default, Caddy uses the local
|
|
|
|
// disk file system.
|
|
|
|
//
|
|
|
|
// File system modules used here must adhere to the following requirements:
|
2022-09-05 18:25:34 -05:00
|
|
|
// - Implement fs.FS interface.
|
2022-07-30 14:07:44 -05:00
|
|
|
// - Support seeking on opened files; i.e.returned fs.File values must
|
|
|
|
// implement the io.Seeker interface. This is required for determining
|
|
|
|
// Content-Length and satisfying Range requests.
|
|
|
|
// - fs.File values that represent directories must implement the
|
|
|
|
// fs.ReadDirFile interface so that directory listings can be procured.
|
|
|
|
FileSystemRaw json.RawMessage `json:"file_system,omitempty" caddy:"namespace=caddy.fs inline_key=backend"`
|
2022-09-05 18:25:34 -05:00
|
|
|
fileSystem fs.FS
|
2022-07-30 14:07:44 -05:00
|
|
|
|
2019-12-23 14:45:35 -05:00
|
|
|
// The path to the root of the site. Default is `{http.vars.root}` if set,
|
2022-07-30 14:07:44 -05:00
|
|
|
// or current working directory otherwise. This should be a trusted value.
|
|
|
|
//
|
|
|
|
// Note that a site root is not a sandbox. Although the file server does
|
|
|
|
// sanitize the request URI to prevent directory traversal, files (including
|
|
|
|
// links) within the site root may be directly accessed based on the request
|
|
|
|
// path. Files and folders within the root should be secure and trustworthy.
|
2019-12-23 14:45:35 -05:00
|
|
|
Root string `json:"root,omitempty"`
|
|
|
|
|
|
|
|
// A list of files or folders to hide; the file server will pretend as if
|
2021-12-06 01:48:40 -05:00
|
|
|
// they don't exist. Accepts globular patterns like `*.ext` or `/foo/*/bar`
|
2020-11-02 16:20:12 -05:00
|
|
|
// as well as placeholders. Because site roots can be dynamic, this list
|
|
|
|
// uses file system paths, not request paths. To clarify, the base of
|
|
|
|
// relative paths is the current working directory, NOT the site root.
|
|
|
|
//
|
|
|
|
// Entries without a path separator (`/` or `\` depending on OS) will match
|
|
|
|
// any file or directory of that name regardless of its path. To hide only a
|
|
|
|
// specific file with a name that may not be unique, always use a path
|
|
|
|
// separator. For example, to hide all files or folder trees named "hidden",
|
|
|
|
// put "hidden" in the list. To hide only ./hidden, put "./hidden" in the list.
|
|
|
|
//
|
|
|
|
// When possible, all paths are resolved to their absolute form before
|
|
|
|
// comparisons are made. For maximum clarity and explictness, use complete,
|
|
|
|
// absolute paths; or, for greater portability, use relative paths instead.
|
2019-12-23 14:45:35 -05:00
|
|
|
Hide []string `json:"hide,omitempty"`
|
|
|
|
|
|
|
|
// The names of files to try as index files if a folder is requested.
|
2022-07-30 14:07:44 -05:00
|
|
|
// Default: index.html, index.txt.
|
2019-12-23 14:45:35 -05:00
|
|
|
IndexNames []string `json:"index_names,omitempty"`
|
|
|
|
|
|
|
|
// Enables file listings if a directory was requested and no index
|
|
|
|
// file is present.
|
|
|
|
Browse *Browse `json:"browse,omitempty"`
|
|
|
|
|
|
|
|
// Use redirects to enforce trailing slashes for directories, or to
|
|
|
|
// remove trailing slash from URIs for files. Default is true.
|
2021-06-25 12:33:18 -05:00
|
|
|
//
|
|
|
|
// Canonicalization will not happen if the last element of the request's
|
|
|
|
// path (the filename) is changed in an internal rewrite, to avoid
|
|
|
|
// clobbering the explicit rewrite with implicit behavior.
|
2019-12-23 14:45:35 -05:00
|
|
|
CanonicalURIs *bool `json:"canonical_uris,omitempty"`
|
|
|
|
|
2021-04-08 12:09:12 -05:00
|
|
|
// Override the status code written when successfully serving a file.
|
|
|
|
// Particularly useful when explicitly serving a file as display for
|
|
|
|
// an error, like a 404 page. A placeholder may be used. By default,
|
|
|
|
// the status code will typically be 200, or 206 for partial content.
|
|
|
|
StatusCode caddyhttp.WeakString `json:"status_code,omitempty"`
|
|
|
|
|
2019-12-23 14:45:35 -05:00
|
|
|
// If pass-thru mode is enabled and a requested file is not found,
|
|
|
|
// it will invoke the next handler in the chain instead of returning
|
|
|
|
// a 404 error. By default, this is false (disabled).
|
|
|
|
PassThru bool `json:"pass_thru,omitempty"`
|
2020-11-26 11:37:42 -05:00
|
|
|
|
2021-03-29 19:47:19 -05:00
|
|
|
// Selection of encoders to use to check for precompressed files.
|
|
|
|
PrecompressedRaw caddy.ModuleMap `json:"precompressed,omitempty" caddy:"namespace=http.precompressed"`
|
|
|
|
|
|
|
|
// If the client has no strong preference (q-factor), choose these encodings in order.
|
|
|
|
// If no order specified here, the first encoding from the Accept-Encoding header
|
|
|
|
// that both client and server support is used
|
|
|
|
PrecompressedOrder []string `json:"precompressed_order,omitempty"`
|
2022-07-30 14:07:44 -05:00
|
|
|
precompressors map[string]encode.Precompressed
|
2021-03-29 19:47:19 -05:00
|
|
|
|
2020-11-26 11:37:42 -05:00
|
|
|
logger *zap.Logger
|
2019-03-31 21:41:29 -05:00
|
|
|
}
|
|
|
|
|
2019-08-21 11:46:35 -05:00
|
|
|
// CaddyModule returns the Caddy module information.
|
|
|
|
func (FileServer) CaddyModule() caddy.ModuleInfo {
|
|
|
|
return caddy.ModuleInfo{
|
2019-12-10 15:36:46 -05:00
|
|
|
ID: "http.handlers.file_server",
|
|
|
|
New: func() caddy.Module { return new(FileServer) },
|
2019-08-21 11:46:35 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-20 11:59:20 -05:00
|
|
|
// Provision sets up the static files responder.
|
2019-06-14 12:58:28 -05:00
|
|
|
func (fsrv *FileServer) Provision(ctx caddy.Context) error {
|
2022-09-16 17:55:30 -05:00
|
|
|
fsrv.logger = ctx.Logger()
|
2020-11-26 11:37:42 -05:00
|
|
|
|
2022-07-30 14:07:44 -05:00
|
|
|
// establish which file system (possibly a virtual one) we'll be using
|
|
|
|
if len(fsrv.FileSystemRaw) > 0 {
|
|
|
|
mod, err := ctx.LoadModule(fsrv, "FileSystemRaw")
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("loading file system module: %v", err)
|
|
|
|
}
|
2022-09-05 18:25:34 -05:00
|
|
|
fsrv.fileSystem = mod.(fs.FS)
|
2022-07-30 14:07:44 -05:00
|
|
|
}
|
|
|
|
if fsrv.fileSystem == nil {
|
|
|
|
fsrv.fileSystem = osFS{}
|
|
|
|
}
|
|
|
|
|
2019-09-09 09:21:45 -05:00
|
|
|
if fsrv.Root == "" {
|
|
|
|
fsrv.Root = "{http.vars.root}"
|
|
|
|
}
|
|
|
|
|
2019-05-20 22:21:33 -05:00
|
|
|
if fsrv.IndexNames == nil {
|
|
|
|
fsrv.IndexNames = defaultIndexNames
|
2019-05-20 11:59:20 -05:00
|
|
|
}
|
2019-05-20 16:46:34 -05:00
|
|
|
|
2020-11-02 16:20:12 -05:00
|
|
|
// for hide paths that are static (i.e. no placeholders), we can transform them into
|
|
|
|
// absolute paths before the server starts for very slight performance improvement
|
|
|
|
for i, h := range fsrv.Hide {
|
|
|
|
if !strings.Contains(h, "{") && strings.Contains(h, separator) {
|
|
|
|
if abs, err := filepath.Abs(h); err == nil {
|
|
|
|
fsrv.Hide[i] = abs
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-30 14:07:44 -05:00
|
|
|
// support precompressed sidecar files
|
2021-03-29 19:47:19 -05:00
|
|
|
mods, err := ctx.LoadModule(fsrv, "PrecompressedRaw")
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("loading encoder modules: %v", err)
|
|
|
|
}
|
2022-08-02 15:39:09 -05:00
|
|
|
for modName, modIface := range mods.(map[string]any) {
|
2021-03-29 19:47:19 -05:00
|
|
|
p, ok := modIface.(encode.Precompressed)
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("module %s is not precompressor", modName)
|
|
|
|
}
|
|
|
|
ae := p.AcceptEncoding()
|
|
|
|
if ae == "" {
|
|
|
|
return fmt.Errorf("precompressor does not specify an Accept-Encoding value")
|
|
|
|
}
|
|
|
|
suffix := p.Suffix()
|
|
|
|
if suffix == "" {
|
|
|
|
return fmt.Errorf("precompressor does not specify a Suffix value")
|
|
|
|
}
|
|
|
|
if _, ok := fsrv.precompressors[ae]; ok {
|
|
|
|
return fmt.Errorf("precompressor already added: %s", ae)
|
|
|
|
}
|
|
|
|
if fsrv.precompressors == nil {
|
|
|
|
fsrv.precompressors = make(map[string]encode.Precompressed)
|
|
|
|
}
|
|
|
|
fsrv.precompressors[ae] = p
|
|
|
|
}
|
|
|
|
|
2019-03-31 21:41:29 -05:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-11-15 19:32:13 -05:00
|
|
|
func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
2019-12-29 15:12:52 -05:00
|
|
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
2019-05-20 11:59:20 -05:00
|
|
|
|
2022-10-18 22:55:25 -05:00
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
// reject paths with Alternate Data Streams (ADS)
|
|
|
|
if strings.Contains(r.URL.Path, ":") {
|
|
|
|
return caddyhttp.Error(http.StatusBadRequest, fmt.Errorf("illegal ADS path"))
|
|
|
|
}
|
|
|
|
// reject paths with "8.3" short names
|
|
|
|
trimmedPath := strings.TrimRight(r.URL.Path, ". ") // Windows ignores trailing dots and spaces, sigh
|
|
|
|
if len(path.Base(trimmedPath)) <= 12 && strings.Contains(trimmedPath, "~") {
|
|
|
|
return caddyhttp.Error(http.StatusBadRequest, fmt.Errorf("illegal short name"))
|
|
|
|
}
|
|
|
|
// both of those could bypass file hiding or possibly leak information even if the file is not hidden
|
|
|
|
}
|
|
|
|
|
2019-05-20 22:21:33 -05:00
|
|
|
filesToHide := fsrv.transformHidePaths(repl)
|
|
|
|
|
2019-08-09 13:05:47 -05:00
|
|
|
root := repl.ReplaceAll(fsrv.Root, ".")
|
2022-08-16 09:48:57 -05:00
|
|
|
|
2023-03-03 16:45:17 -05:00
|
|
|
// remove any trailing `/` as it breaks fs.ValidPath() in the stdlib
|
|
|
|
filename := strings.TrimSuffix(caddyhttp.SanitizedPathJoin(root, r.URL.Path), "/")
|
2019-05-20 11:59:20 -05:00
|
|
|
|
2020-11-26 11:37:42 -05:00
|
|
|
fsrv.logger.Debug("sanitized path join",
|
|
|
|
zap.String("site_root", root),
|
2021-03-01 15:49:13 -05:00
|
|
|
zap.String("request_path", r.URL.Path),
|
2020-11-26 11:37:42 -05:00
|
|
|
zap.String("result", filename))
|
|
|
|
|
2019-05-20 11:59:20 -05:00
|
|
|
// get information about the file
|
2022-09-05 18:25:34 -05:00
|
|
|
info, err := fs.Stat(fsrv.fileSystem, filename)
|
2019-05-20 11:59:20 -05:00
|
|
|
if err != nil {
|
2022-07-30 14:07:44 -05:00
|
|
|
err = fsrv.mapDirOpenError(err, filename)
|
2022-10-04 22:32:40 -05:00
|
|
|
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrInvalid) {
|
2019-11-15 19:32:13 -05:00
|
|
|
return fsrv.notFound(w, r, next)
|
2022-07-30 14:07:44 -05:00
|
|
|
} else if errors.Is(err, fs.ErrPermission) {
|
2019-05-20 11:59:20 -05:00
|
|
|
return caddyhttp.Error(http.StatusForbidden, err)
|
|
|
|
}
|
|
|
|
return caddyhttp.Error(http.StatusInternalServerError, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// if the request mapped to a directory, see if
|
|
|
|
// there is an index file we can serve
|
2019-09-09 09:21:45 -05:00
|
|
|
var implicitIndexFile bool
|
2019-05-20 22:21:33 -05:00
|
|
|
if info.IsDir() && len(fsrv.IndexNames) > 0 {
|
|
|
|
for _, indexPage := range fsrv.IndexNames {
|
2022-04-07 16:01:09 -05:00
|
|
|
indexPage := repl.ReplaceAll(indexPage, "")
|
2021-06-17 10:59:08 -05:00
|
|
|
indexPath := caddyhttp.SanitizedPathJoin(filename, indexPage)
|
2019-05-20 16:46:34 -05:00
|
|
|
if fileHidden(indexPath, filesToHide) {
|
2019-05-20 11:59:20 -05:00
|
|
|
// pretend this file doesn't exist
|
2020-11-26 11:37:42 -05:00
|
|
|
fsrv.logger.Debug("hiding index file",
|
|
|
|
zap.String("filename", indexPath),
|
|
|
|
zap.Strings("files_to_hide", filesToHide))
|
2019-05-20 11:59:20 -05:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2022-09-05 18:25:34 -05:00
|
|
|
indexInfo, err := fs.Stat(fsrv.fileSystem, indexPath)
|
2019-05-20 11:59:20 -05:00
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2019-09-09 09:21:45 -05:00
|
|
|
// don't rewrite the request path to append
|
|
|
|
// the index file, because we might need to
|
|
|
|
// do a canonical-URL redirect below based
|
|
|
|
// on the URL as-is
|
2019-05-20 11:59:20 -05:00
|
|
|
|
2019-09-09 09:21:45 -05:00
|
|
|
// we've chosen to use this index file,
|
|
|
|
// so replace the last file info and path
|
|
|
|
// with that of the index file
|
2019-05-20 11:59:20 -05:00
|
|
|
info = indexInfo
|
2019-05-20 16:46:34 -05:00
|
|
|
filename = indexPath
|
2019-09-09 09:21:45 -05:00
|
|
|
implicitIndexFile = true
|
2020-11-26 11:37:42 -05:00
|
|
|
fsrv.logger.Debug("located index file", zap.String("filename", filename))
|
2019-05-20 11:59:20 -05:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// if still referencing a directory, delegate
|
|
|
|
// to browse or return an error
|
|
|
|
if info.IsDir() {
|
2020-11-26 11:37:42 -05:00
|
|
|
fsrv.logger.Debug("no index file in directory",
|
|
|
|
zap.String("path", filename),
|
|
|
|
zap.Strings("index_filenames", fsrv.IndexNames))
|
2019-05-20 22:21:33 -05:00
|
|
|
if fsrv.Browse != nil && !fileHidden(filename, filesToHide) {
|
2020-11-24 14:24:44 -05:00
|
|
|
return fsrv.serveBrowse(root, filename, w, r, next)
|
2019-05-20 11:59:20 -05:00
|
|
|
}
|
2019-11-15 19:32:13 -05:00
|
|
|
return fsrv.notFound(w, r, next)
|
2019-05-20 11:59:20 -05:00
|
|
|
}
|
|
|
|
|
2019-05-20 22:21:33 -05:00
|
|
|
// one last check to ensure the file isn't hidden (we might
|
|
|
|
// have changed the filename from when we last checked)
|
|
|
|
if fileHidden(filename, filesToHide) {
|
2020-11-26 11:37:42 -05:00
|
|
|
fsrv.logger.Debug("hiding file",
|
|
|
|
zap.String("filename", filename),
|
|
|
|
zap.Strings("files_to_hide", filesToHide))
|
2019-11-15 19:32:13 -05:00
|
|
|
return fsrv.notFound(w, r, next)
|
2019-05-20 22:21:33 -05:00
|
|
|
}
|
|
|
|
|
2019-09-09 09:21:45 -05:00
|
|
|
// if URL canonicalization is enabled, we need to enforce trailing
|
|
|
|
// slash convention: if a directory, trailing slash; if a file, no
|
|
|
|
// trailing slash - not enforcing this can break relative hrefs
|
|
|
|
// in HTML (see https://github.com/caddyserver/caddy/issues/2741)
|
|
|
|
if fsrv.CanonicalURIs == nil || *fsrv.CanonicalURIs {
|
fileserver: Only redirect if filename not rewritten (fix #4205)
This is the more correct implementation of 23dadc0d86dd75dad7559c25f20c9641bc7bc30f (#4179)... I think. This commit effectively undoes the revert in 8848df9c5d372a559d01512b7a4ef00e38867b55, but with corrections to the logic.
We *do* need to use the original request path (the path the browser knows) for redirects, since they are external, and rewrites are only internal.
However, if the path was rewritten to a non-canonical path, we should not redirect to canonicalize that, since rewrites are intentional by the site owner. Canonicalizing the path involves modifying only the suffix (base element, or filename) of the path. Thus, if a rewrite involves only the prefix (like how handle_path strips a path prefix), then we can (hopefully!) safely redirect using the original URI since the filename was not rewritten.
So basically, if rewrites modify the filename, we should not canonicalize those requests. If rewrites only modify another part of the path (commonly a prefix), we should be OK to redirect.
2021-06-17 10:55:49 -05:00
|
|
|
// Only redirect if the last element of the path (the filename) was not
|
|
|
|
// rewritten; if the admin wanted to rewrite to the canonical path, they
|
|
|
|
// would have, and we have to be very careful not to introduce unwanted
|
|
|
|
// redirects and especially redirect loops!
|
|
|
|
// See https://github.com/caddyserver/caddy/issues/4205.
|
|
|
|
origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
|
|
|
|
if path.Base(origReq.URL.Path) == path.Base(r.URL.Path) {
|
|
|
|
if implicitIndexFile && !strings.HasSuffix(origReq.URL.Path, "/") {
|
|
|
|
to := origReq.URL.Path + "/"
|
|
|
|
fsrv.logger.Debug("redirecting to canonical URI (adding trailing slash for directory)",
|
|
|
|
zap.String("from_path", origReq.URL.Path),
|
|
|
|
zap.String("to_path", to))
|
|
|
|
return redirect(w, r, to)
|
|
|
|
} else if !implicitIndexFile && strings.HasSuffix(origReq.URL.Path, "/") {
|
|
|
|
to := origReq.URL.Path[:len(origReq.URL.Path)-1]
|
|
|
|
fsrv.logger.Debug("redirecting to canonical URI (removing trailing slash for file)",
|
|
|
|
zap.String("from_path", origReq.URL.Path),
|
|
|
|
zap.String("to_path", to))
|
|
|
|
return redirect(w, r, to)
|
|
|
|
}
|
2019-09-09 09:21:45 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-30 14:07:44 -05:00
|
|
|
var file fs.File
|
2023-05-20 18:21:43 -05:00
|
|
|
|
|
|
|
// etag is usually unset, but if the user knows what they're doing, let them override it
|
|
|
|
etag := w.Header().Get("Etag")
|
2020-11-26 11:37:42 -05:00
|
|
|
|
2021-03-29 19:47:19 -05:00
|
|
|
// check for precompressed files
|
|
|
|
for _, ae := range encode.AcceptedEncodings(r, fsrv.PrecompressedOrder) {
|
|
|
|
precompress, ok := fsrv.precompressors[ae]
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
compressedFilename := filename + precompress.Suffix()
|
2022-09-05 18:25:34 -05:00
|
|
|
compressedInfo, err := fs.Stat(fsrv.fileSystem, compressedFilename)
|
2021-03-29 19:47:19 -05:00
|
|
|
if err != nil || compressedInfo.IsDir() {
|
|
|
|
fsrv.logger.Debug("precompressed file not accessible", zap.String("filename", compressedFilename), zap.Error(err))
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
fsrv.logger.Debug("opening compressed sidecar file", zap.String("filename", compressedFilename), zap.Error(err))
|
|
|
|
file, err = fsrv.openFile(compressedFilename, w)
|
|
|
|
if err != nil {
|
|
|
|
fsrv.logger.Warn("opening precompressed file failed", zap.String("filename", compressedFilename), zap.Error(err))
|
|
|
|
if caddyErr, ok := err.(caddyhttp.HandlerError); ok && caddyErr.StatusCode == http.StatusServiceUnavailable {
|
|
|
|
return err
|
|
|
|
}
|
2022-08-08 14:09:57 -05:00
|
|
|
file = nil
|
2021-03-29 19:47:19 -05:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
w.Header().Set("Content-Encoding", ae)
|
|
|
|
w.Header().Del("Accept-Ranges")
|
|
|
|
w.Header().Add("Vary", "Accept-Encoding")
|
2022-08-08 14:09:57 -05:00
|
|
|
|
|
|
|
// 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
|
2023-05-20 18:21:43 -05:00
|
|
|
if etag == "" {
|
|
|
|
etag = calculateEtag(compressedInfo)
|
|
|
|
}
|
2022-08-08 14:09:57 -05:00
|
|
|
|
2021-03-29 19:47:19 -05:00
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// no precompressed file found, use the actual file
|
|
|
|
if file == nil {
|
|
|
|
fsrv.logger.Debug("opening file", zap.String("filename", filename))
|
|
|
|
|
|
|
|
// open the file
|
|
|
|
file, err = fsrv.openFile(filename, w)
|
|
|
|
if err != nil {
|
|
|
|
if herr, ok := err.(caddyhttp.HandlerError); ok &&
|
|
|
|
herr.StatusCode == http.StatusNotFound {
|
|
|
|
return fsrv.notFound(w, r, next)
|
|
|
|
}
|
|
|
|
return err // error is already structured
|
2019-11-15 19:32:13 -05:00
|
|
|
}
|
2021-03-29 19:47:19 -05:00
|
|
|
defer file.Close()
|
2022-08-08 14:09:57 -05:00
|
|
|
|
2023-05-20 18:21:43 -05:00
|
|
|
if etag == "" {
|
|
|
|
etag = calculateEtag(info)
|
|
|
|
}
|
2019-05-20 11:59:20 -05:00
|
|
|
}
|
|
|
|
|
2022-10-24 11:23:57 -05:00
|
|
|
// 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 {
|
2023-08-09 12:12:09 -05:00
|
|
|
// 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)
|
|
|
|
}
|
2022-10-24 11:23:57 -05:00
|
|
|
}
|
|
|
|
|
2022-08-08 14:09:57 -05:00
|
|
|
// set the Etag - note that a conditional If-None-Match request is handled
|
|
|
|
// by http.ServeContent below, which checks against this Etag value
|
2023-05-20 18:21:43 -05:00
|
|
|
if etag != "" {
|
|
|
|
w.Header().Set("Etag", etag)
|
|
|
|
}
|
2019-05-20 11:59:20 -05:00
|
|
|
|
2019-06-07 20:59:17 -05:00
|
|
|
if w.Header().Get("Content-Type") == "" {
|
2019-06-21 15:36:26 -05:00
|
|
|
mtyp := mime.TypeByExtension(filepath.Ext(filename))
|
|
|
|
if mtyp == "" {
|
2022-07-30 14:07:44 -05:00
|
|
|
// do not allow Go to sniff the content-type; see https://www.youtube.com/watch?v=8t8JYpt0egE
|
2019-06-21 15:36:26 -05:00
|
|
|
w.Header()["Content-Type"] = nil
|
|
|
|
} else {
|
|
|
|
w.Header().Set("Content-Type", mtyp)
|
|
|
|
}
|
2019-06-07 20:59:17 -05:00
|
|
|
}
|
2019-05-20 22:21:33 -05:00
|
|
|
|
2021-04-29 01:01:48 -05:00
|
|
|
var statusCodeOverride int
|
|
|
|
|
|
|
|
// if this handler exists in an error context (i.e. is part of a
|
|
|
|
// handler chain that is supposed to handle a previous error),
|
|
|
|
// we should set status code to the one from the error instead
|
|
|
|
// of letting http.ServeContent set the default (usually 200)
|
2019-10-28 15:39:37 -05:00
|
|
|
if reqErr, ok := r.Context().Value(caddyhttp.ErrorCtxKey).(error); ok {
|
2021-04-29 01:01:48 -05:00
|
|
|
statusCodeOverride = http.StatusInternalServerError
|
2019-10-28 15:39:37 -05:00
|
|
|
if handlerErr, ok := reqErr.(caddyhttp.HandlerError); ok {
|
|
|
|
if handlerErr.StatusCode > 0 {
|
2021-04-29 01:01:48 -05:00
|
|
|
statusCodeOverride = handlerErr.StatusCode
|
2019-10-28 15:39:37 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-29 01:01:48 -05:00
|
|
|
// if a status code override is configured, run the replacer on it
|
2021-04-08 12:09:12 -05:00
|
|
|
if codeStr := fsrv.StatusCode.String(); codeStr != "" {
|
2021-04-29 01:01:48 -05:00
|
|
|
statusCodeOverride, err = strconv.Atoi(repl.ReplaceAll(codeStr, ""))
|
2021-04-08 12:09:12 -05:00
|
|
|
if err != nil {
|
|
|
|
return caddyhttp.Error(http.StatusInternalServerError, err)
|
|
|
|
}
|
2021-04-29 01:01:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// if we do have an override from the previous two parts, then
|
|
|
|
// we wrap the response writer to intercept the WriteHeader call
|
|
|
|
if statusCodeOverride > 0 {
|
|
|
|
w = statusOverrideResponseWriter{ResponseWriter: w, code: statusCodeOverride}
|
2021-04-08 12:09:12 -05:00
|
|
|
}
|
|
|
|
|
2019-05-20 11:59:20 -05:00
|
|
|
// let the standard library do what it does best; note, however,
|
|
|
|
// that errors generated by ServeContent are written immediately
|
2019-06-21 15:36:26 -05:00
|
|
|
// to the response, so we cannot handle them (but errors there
|
2019-05-20 16:46:34 -05:00
|
|
|
// are rare)
|
2022-07-30 14:07:44 -05:00
|
|
|
http.ServeContent(w, r, info.Name(), info.ModTime(), file.(io.ReadSeeker))
|
2019-05-20 11:59:20 -05:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-05-20 16:46:34 -05:00
|
|
|
// 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).
|
2022-07-30 14:07:44 -05:00
|
|
|
func (fsrv *FileServer) openFile(filename string, w http.ResponseWriter) (fs.File, error) {
|
|
|
|
file, err := fsrv.fileSystem.Open(filename)
|
2019-05-20 16:46:34 -05:00
|
|
|
if err != nil {
|
2022-07-30 14:07:44 -05:00
|
|
|
err = fsrv.mapDirOpenError(err, filename)
|
2019-05-20 16:46:34 -05:00
|
|
|
if os.IsNotExist(err) {
|
2021-03-19 12:42:26 -05:00
|
|
|
fsrv.logger.Debug("file not found", zap.String("filename", filename), zap.Error(err))
|
2019-05-20 16:46:34 -05:00
|
|
|
return nil, caddyhttp.Error(http.StatusNotFound, err)
|
|
|
|
} else if os.IsPermission(err) {
|
2021-03-19 12:42:26 -05:00
|
|
|
fsrv.logger.Debug("permission denied", zap.String("filename", filename), zap.Error(err))
|
2019-05-20 16:46:34 -05:00
|
|
|
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
|
2020-11-22 16:50:29 -05:00
|
|
|
//nolint:gosec
|
2019-05-20 16:46:34 -05:00
|
|
|
backoff := weakrand.Intn(maxBackoff-minBackoff) + minBackoff
|
|
|
|
w.Header().Set("Retry-After", strconv.Itoa(backoff))
|
2021-03-19 12:42:26 -05:00
|
|
|
fsrv.logger.Debug("retry after backoff", zap.String("filename", filename), zap.Int("backoff", backoff), zap.Error(err))
|
2019-05-20 16:46:34 -05:00
|
|
|
return nil, caddyhttp.Error(http.StatusServiceUnavailable, err)
|
|
|
|
}
|
|
|
|
return file, nil
|
|
|
|
}
|
|
|
|
|
2019-05-20 18:15:38 -05:00
|
|
|
// 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/
|
2022-07-30 14:07:44 -05:00
|
|
|
func (fsrv *FileServer) mapDirOpenError(originalErr error, name string) error {
|
|
|
|
if errors.Is(originalErr, fs.ErrNotExist) || errors.Is(originalErr, fs.ErrPermission) {
|
2019-05-20 18:15:38 -05:00
|
|
|
return originalErr
|
|
|
|
}
|
|
|
|
|
2020-11-02 16:20:12 -05:00
|
|
|
parts := strings.Split(name, separator)
|
2019-05-20 18:15:38 -05:00
|
|
|
for i := range parts {
|
|
|
|
if parts[i] == "" {
|
|
|
|
continue
|
|
|
|
}
|
2022-09-05 18:25:34 -05:00
|
|
|
fi, err := fs.Stat(fsrv.fileSystem, strings.Join(parts[:i+1], separator))
|
2019-05-20 18:15:38 -05:00
|
|
|
if err != nil {
|
|
|
|
return originalErr
|
|
|
|
}
|
|
|
|
if !fi.IsDir() {
|
2022-07-30 14:07:44 -05:00
|
|
|
return fs.ErrNotExist
|
2019-05-20 18:15:38 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return originalErr
|
|
|
|
}
|
|
|
|
|
2020-11-02 16:20:12 -05:00
|
|
|
// transformHidePaths performs replacements for all the elements of fsrv.Hide and
|
|
|
|
// makes them absolute paths (if they contain a path separator), then returns a
|
|
|
|
// new list of the transformed values.
|
2019-12-29 15:12:52 -05:00
|
|
|
func (fsrv *FileServer) transformHidePaths(repl *caddy.Replacer) []string {
|
2019-05-20 22:21:33 -05:00
|
|
|
hide := make([]string, len(fsrv.Hide))
|
|
|
|
for i := range fsrv.Hide {
|
|
|
|
hide[i] = repl.ReplaceAll(fsrv.Hide[i], "")
|
2020-11-02 16:20:12 -05:00
|
|
|
if strings.Contains(hide[i], separator) {
|
|
|
|
abs, err := filepath.Abs(hide[i])
|
|
|
|
if err == nil {
|
|
|
|
hide[i] = abs
|
|
|
|
}
|
|
|
|
}
|
2019-05-20 11:59:20 -05:00
|
|
|
}
|
|
|
|
return hide
|
|
|
|
}
|
|
|
|
|
2020-11-02 16:20:12 -05:00
|
|
|
// fileHidden returns true if filename is hidden according to the hide list.
|
|
|
|
// filename must be a relative or absolute file system path, not a request
|
|
|
|
// URI path. It is expected that all the paths in the hide list are absolute
|
|
|
|
// paths or are singular filenames (without a path separator).
|
2019-05-20 16:46:34 -05:00
|
|
|
func fileHidden(filename string, hide []string) bool {
|
2020-11-02 16:20:12 -05:00
|
|
|
if len(hide) == 0 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// all path comparisons use the complete absolute path if possible
|
|
|
|
filenameAbs, err := filepath.Abs(filename)
|
|
|
|
if err == nil {
|
|
|
|
filename = filenameAbs
|
|
|
|
}
|
|
|
|
|
2020-09-11 13:20:39 -05:00
|
|
|
var components []string
|
2019-05-20 11:59:20 -05:00
|
|
|
|
|
|
|
for _, h := range hide {
|
2020-11-02 16:20:12 -05:00
|
|
|
if !strings.Contains(h, separator) {
|
2020-09-11 13:20:39 -05:00
|
|
|
// if there is no separator in h, then we assume the user
|
|
|
|
// wants to hide any files or folders that match that
|
|
|
|
// name; thus we have to compare against each component
|
|
|
|
// of the filename, e.g. hiding "bar" would hide "/bar"
|
|
|
|
// as well as "/foo/bar/baz" but not "/barstool".
|
|
|
|
if len(components) == 0 {
|
2020-11-02 16:20:12 -05:00
|
|
|
components = strings.Split(filename, separator)
|
2020-09-11 13:20:39 -05:00
|
|
|
}
|
|
|
|
for _, c := range components {
|
2020-11-02 16:20:12 -05:00
|
|
|
if hidden, _ := filepath.Match(h, c); hidden {
|
2020-09-11 13:20:39 -05:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if strings.HasPrefix(filename, h) {
|
2020-11-02 16:20:12 -05:00
|
|
|
// if there is a separator in h, and filename is exactly
|
|
|
|
// prefixed with h, then we can do a prefix match so that
|
|
|
|
// "/foo" matches "/foo/bar" but not "/foobar".
|
2020-09-11 13:20:39 -05:00
|
|
|
withoutPrefix := strings.TrimPrefix(filename, h)
|
2020-11-02 16:20:12 -05:00
|
|
|
if strings.HasPrefix(withoutPrefix, separator) {
|
2019-05-20 11:59:20 -05:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
2020-09-11 13:20:39 -05:00
|
|
|
|
|
|
|
// in the general case, a glob match will suffice
|
|
|
|
if hidden, _ := filepath.Match(h, filename); hidden {
|
2019-05-20 11:59:20 -05:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2019-11-15 19:32:13 -05:00
|
|
|
// notFound returns a 404 error or, if pass-thru is enabled,
|
|
|
|
// it calls the next handler in the chain.
|
|
|
|
func (fsrv *FileServer) notFound(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
|
|
|
if fsrv.PassThru {
|
|
|
|
return next.ServeHTTP(w, r)
|
|
|
|
}
|
|
|
|
return caddyhttp.Error(http.StatusNotFound, nil)
|
|
|
|
}
|
|
|
|
|
2019-06-27 14:09:10 -05:00
|
|
|
// calculateEtag produces a strong etag by default, although, for
|
|
|
|
// efficiency reasons, it does not actually consume the contents
|
|
|
|
// of the file to make a hash of all the bytes. ¯\_(ツ)_/¯
|
|
|
|
// 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 {
|
2023-05-22 15:17:15 -05:00
|
|
|
mtime := d.ModTime().Unix()
|
|
|
|
if mtime == 0 || mtime == 1 {
|
|
|
|
return "" // not useful anyway; see issue #5548
|
|
|
|
}
|
|
|
|
t := strconv.FormatInt(mtime, 36)
|
2019-06-27 14:09:10 -05:00
|
|
|
s := strconv.FormatInt(d.Size(), 36)
|
|
|
|
return `"` + t + s + `"`
|
|
|
|
}
|
|
|
|
|
2019-09-09 09:21:45 -05:00
|
|
|
func redirect(w http.ResponseWriter, r *http.Request, to string) error {
|
|
|
|
for strings.HasPrefix(to, "//") {
|
|
|
|
// prevent path-based open redirects
|
|
|
|
to = strings.TrimPrefix(to, "/")
|
|
|
|
}
|
|
|
|
http.Redirect(w, r, to, http.StatusPermanentRedirect)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-04-29 01:01:48 -05:00
|
|
|
// statusOverrideResponseWriter intercepts WriteHeader calls
|
|
|
|
// to instead write the HTTP status code we want instead
|
|
|
|
// of the one http.ServeContent will use by default (usually 200)
|
|
|
|
type statusOverrideResponseWriter struct {
|
|
|
|
http.ResponseWriter
|
|
|
|
code int
|
|
|
|
}
|
|
|
|
|
|
|
|
// WriteHeader intercepts calls by the stdlib to WriteHeader
|
|
|
|
// to instead write the HTTP status code we want.
|
|
|
|
func (wr statusOverrideResponseWriter) WriteHeader(int) {
|
|
|
|
wr.ResponseWriter.WriteHeader(wr.code)
|
|
|
|
}
|
|
|
|
|
2023-04-26 18:44:01 -05:00
|
|
|
// Unwrap returns the underlying ResponseWriter, necessary for
|
|
|
|
// http.ResponseController to work correctly.
|
|
|
|
func (wr statusOverrideResponseWriter) Unwrap() http.ResponseWriter {
|
|
|
|
return wr.ResponseWriter
|
|
|
|
}
|
|
|
|
|
2022-09-05 18:25:34 -05:00
|
|
|
// osFS is a simple fs.FS implementation that uses the local
|
2022-07-30 14:07:44 -05:00
|
|
|
// file system. (We do not use os.DirFS because we do our own
|
|
|
|
// rooting or path prefixing without being constrained to a single
|
|
|
|
// root folder. The standard os.DirFS implementation is problematic
|
|
|
|
// since roots can be dynamic in our application.)
|
2022-09-05 14:53:41 -05:00
|
|
|
//
|
2022-09-05 18:25:34 -05:00
|
|
|
// osFS also implements fs.StatFS, fs.GlobFS, fs.ReadDirFS, and fs.ReadFileFS.
|
2022-07-30 14:07:44 -05:00
|
|
|
type osFS struct{}
|
|
|
|
|
2022-09-05 14:53:41 -05:00
|
|
|
func (osFS) Open(name string) (fs.File, error) { return os.Open(name) }
|
|
|
|
func (osFS) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) }
|
|
|
|
func (osFS) Glob(pattern string) ([]string, error) { return filepath.Glob(pattern) }
|
|
|
|
func (osFS) ReadDir(name string) ([]fs.DirEntry, error) { return os.ReadDir(name) }
|
|
|
|
func (osFS) ReadFile(name string) ([]byte, error) { return os.ReadFile(name) }
|
2022-07-30 14:07:44 -05:00
|
|
|
|
2019-08-09 13:05:47 -05:00
|
|
|
var defaultIndexNames = []string{"index.html", "index.txt"}
|
2019-05-20 11:59:20 -05:00
|
|
|
|
2020-11-02 16:20:12 -05:00
|
|
|
const (
|
|
|
|
minBackoff, maxBackoff = 2, 5
|
|
|
|
separator = string(filepath.Separator)
|
|
|
|
)
|
2019-05-20 11:59:20 -05:00
|
|
|
|
2019-07-07 15:12:22 -05:00
|
|
|
// Interface guards
|
|
|
|
var (
|
2019-07-09 13:58:39 -05:00
|
|
|
_ caddy.Provisioner = (*FileServer)(nil)
|
|
|
|
_ caddyhttp.MiddlewareHandler = (*FileServer)(nil)
|
2022-09-05 14:53:41 -05:00
|
|
|
|
2022-09-05 18:25:34 -05:00
|
|
|
_ fs.StatFS = (*osFS)(nil)
|
2022-09-05 14:53:41 -05:00
|
|
|
_ fs.GlobFS = (*osFS)(nil)
|
|
|
|
_ fs.ReadDirFS = (*osFS)(nil)
|
|
|
|
_ fs.ReadFileFS = (*osFS)(nil)
|
2019-07-07 15:12:22 -05:00
|
|
|
)
|