Go API Documentation

github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver

No package summary is available.

Package

Files: 6. Third party imports: 11. Imports from organisation: 1. Tests: 0. Benchmarks: 0.

Constants

const (
	sortByName		= "name"
	sortByNameDirFirst	= "namedirfirst"
	sortBySize		= "size"
	sortByTime		= "time"

	sortOrderAsc	= "asc"
	sortOrderDesc	= "desc"
)
const (
	tryPolicyFirstExist		= "first_exist"
	tryPolicyFirstExistFallback	= "first_exist_fallback"
	tryPolicyLargestSize		= "largest_size"
	tryPolicySmallestSize		= "smallest_size"
	tryPolicyMostRecentlyMod	= "most_recently_modified"
)
const (
	minBackoff, maxBackoff	= 2, 5
	separator		= string(filepath.Separator)
)
const (
	defaultDirEntryLimit = 10000
)

Vars

BrowseTemplate is the default template document to use for file listings. By default, its default value is an embedded document. You can override this value at program start, or if you are running Caddy via config, you can specify a custom template_file in the browse configuration.

var BrowseTemplate string
var _ caddyfile.Unmarshaler = (*FileServer)(nil)

bufPool is used to increase the efficiency of file listings.

var bufPool = sync.Pool{
	New: func() any {
		return new(bytes.Buffer)
	},
}
var defaultIndexNames = []string{"index.html", "index.txt"}

globSafeRepl replaces special glob characters with escaped equivalents. Note that the filepath godoc states that escaping is not done on Windows because of the separator.

var globSafeRepl = strings.NewReplacer(
	"*", "\\*",
	"[", "\\[",
	"?", "\\?",
)

Types

Browse

Browse configures directory browsing.

type Browse struct {
	// Filename of the template to use instead of the embedded browse template.
	TemplateFile	string	`json:"template_file,omitempty"`

	// Determines whether or not targets of symlinks should be revealed.
	RevealSymlinks	bool	`json:"reveal_symlinks,omitempty"`

	// Override the default sort.
	// It includes the following options:
	//   - sort_by: name(default), namedirfirst, size, time
	//   - order: asc(default), desc
	// eg.:
	//   - `sort time desc` will sort by time in descending order
	//   - `sort size` will sort by size in ascending order
	// The first option must be `sort_by` and the second option must be `order` (if exists).
	SortOptions	[]string	`json:"sort,omitempty"`

	// FileLimit limits the number of up to n DirEntry values in directory order.
	FileLimit	int	`json:"file_limit,omitempty"`
}

FileServer

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 are requested if no index file is present. If "browse" is enabled, Caddy may serve a JSON array of the directory 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. 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.

type FileServer struct {
	// The file system implementation to use. By default, Caddy uses the local
	// disk file system.
	//
	// if a non default filesystem is used, it must be first be registered in the globals section.
	FileSystem	string	`json:"fs,omitempty"`

	// The path to the root of the site. Default is `{http.vars.root}` if set,
	// 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.
	Root	string	`json:"root,omitempty"`

	// A list of files or folders to hide; the file server will pretend as if
	// they don't exist. Accepts globular patterns like `*.ext` or `/foo/*/bar`
	// 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.
	Hide	[]string	`json:"hide,omitempty"`

	// The names of files to try as index files if a folder is requested.
	// Default: index.html, index.txt.
	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.
	//
	// 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.
	CanonicalURIs	*bool	`json:"canonical_uris,omitempty"`

	// 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"`

	// 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"`

	// 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"`
	precompressors		map[string]encode.Precompressed

	// List of file extensions to try to read Etags from.
	// If set, file Etags will be read from sidecar files
	// with any of these suffixes, instead of generating
	// our own Etag.
	EtagFileExtensions	[]string	`json:"etag_file_extensions,omitempty"`

	fsmap	caddy.FileSystems

	logger	*zap.Logger
}

MatchFile

MatchFile is an HTTP request matcher that can match requests based upon file existence.

Upon matching, three new placeholders will be made available:

  • {http.matchers.file.relative} The root-relative path of the file. This is often useful when rewriting requests.
  • {http.matchers.file.absolute} The absolute path of the matched file.
  • {http.matchers.file.type} Set to "directory" if the matched file is a directory, "file" otherwise.
  • {http.matchers.file.remainder} Set to the remainder of the path if the path was split by split_path.

Even though file matching may depend on the OS path separator, the placeholder values always use /.

type MatchFile struct {
	// The file system implementation to use. By default, the
	// local disk file system will be used.
	FileSystem	string	`json:"fs,omitempty"`

	// The root directory, used for creating absolute
	// file paths, and required when working with
	// relative paths; if not specified, `{http.vars.root}`
	// will be used, if set; otherwise, the current
	// directory is assumed. Accepts placeholders.
	Root	string	`json:"root,omitempty"`

	// The list of files to try. Each path here is
	// considered related to Root. If nil, the request
	// URL's path will be assumed. Files and
	// directories are treated distinctly, so to match
	// a directory, the filepath MUST end in a forward
	// slash `/`. To match a regular file, there must
	// be no trailing slash. Accepts placeholders. If
	// the policy is "first_exist", then an error may
	// be triggered as a fallback by configuring "="
	// followed by a status code number,
	// for example "=404".
	TryFiles	[]string	`json:"try_files,omitempty"`

	// How to choose a file in TryFiles. Can be:
	//
	// - first_exist
	// - first_exist_fallback
	// - smallest_size
	// - largest_size
	// - most_recently_modified
	//
	// Default is first_exist.
	TryPolicy	string	`json:"try_policy,omitempty"`

	// A list of delimiters to use to split the path in two
	// when trying files. If empty, no splitting will
	// occur, and the path will be tried as-is. For each
	// split value, the left-hand side of the split,
	// including the split value, will be the path tried.
	// For example, the path `/remote.php/dav/` using the
	// split value `.php` would try the file `/remote.php`.
	// Each delimiter must appear at the end of a URI path
	// component in order to be used as a split delimiter.
	SplitPath	[]string	`json:"split_path,omitempty"`

	fsmap	caddy.FileSystems

	logger	*zap.Logger
}

byName, byNameDirFirst, bySize, byTime

This type doesn't have documentation.

type (
	byName		browseTemplateContext
	byNameDirFirst	browseTemplateContext
	bySize		browseTemplateContext
	byTime		browseTemplateContext
)

browseTemplateContext

browseTemplateContext provides the template context for directory listings.

type browseTemplateContext struct {
	// The name of the directory (the last element of the path).
	Name	string	`json:"name"`

	// The full path of the request.
	Path	string	`json:"path"`

	// Whether the parent directory is browsable.
	CanGoUp	bool	`json:"can_go_up"`

	// The items (files and folders) in the path.
	Items	[]fileInfo	`json:"items,omitempty"`

	// If ≠0 then Items starting from that many elements.
	Offset	int	`json:"offset,omitempty"`

	// If ≠0 then Items have been limited to that many elements.
	Limit	int	`json:"limit,omitempty"`

	// The number of directories in the listing.
	NumDirs	int	`json:"num_dirs"`

	// The number of files (items that aren't directories) in the listing.
	NumFiles	int	`json:"num_files"`

	// The total size of all files in the listing. Only includes the
	// size of the files themselves, not the size of symlink targets
	// (i.e. the calculation of this value does not follow symlinks).
	TotalFileSize	int64	`json:"total_file_size"`

	// The total size of all files in the listing, including the
	// size of the files targeted by symlinks.
	TotalFileSizeFollowingSymlinks	int64	`json:"total_file_size_following_symlinks"`

	// Sort column used
	Sort	string	`json:"sort,omitempty"`

	// Sorting order
	Order	string	`json:"order,omitempty"`

	// Display format (list or grid)
	Layout	string	`json:"layout,omitempty"`

	// The most recent file modification date in the listing.
	// Used for HTTP header purposes.
	lastModified	time.Time
}

crumb

crumb represents part of a breadcrumb menu, pairing a link with the text to display.

type crumb struct {
	Link, Text string
}

fileInfo

fileInfo contains serializable information about a file or directory.

type fileInfo struct {
	Name		string		`json:"name"`
	Size		int64		`json:"size"`
	URL		string		`json:"url"`
	ModTime		time.Time	`json:"mod_time"`
	Mode		os.FileMode	`json:"mode"`
	IsDir		bool		`json:"is_dir"`
	IsSymlink	bool		`json:"is_symlink"`
	SymlinkPath	string		`json:"symlink_path,omitempty"`

	// a pointer to the template context is useful inside nested templates
	Tpl	*browseTemplateContext	`json:"-"`
}

statusOverrideResponseWriter

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
}

templateContext

templateContext powers the context used when evaluating the browse template. It combines browse-specific features with the standard templates handler features.

type templateContext struct {
	templates.TemplateContext
	*browseTemplateContext
}

Functions

func (*FileServer) FinalizeUnmarshalCaddyfile

FinalizeUnmarshalCaddyfile finalizes the Caddyfile parsing which requires having an httpcaddyfile.Helper to function, to setup hidden Caddyfiles.

func (fsrv *FileServer) FinalizeUnmarshalCaddyfile(h httpcaddyfile.Helper) error {
	// Hide the Caddyfile (and any imported Caddyfiles).
	// This needs to be done in here instead of UnmarshalCaddyfile
	// because UnmarshalCaddyfile only has access to the dispenser
	// and not the helper, and only the helper has access to the
	// Caddyfiles function.
	if configFiles := h.Caddyfiles(); len(configFiles) > 0 {
		for _, file := range configFiles {
			file = filepath.Clean(file)
			if !fileHidden(file, fsrv.Hide) {
				// if there's no path separator, the file server module will hide all
				// files by that name, rather than a specific one; but we want to hide
				// only this specific file, so ensure there's always a path separator
				if !strings.Contains(file, separator) {
					file = "." + separator + file
				}
				fsrv.Hide = append(fsrv.Hide, file)
			}
		}
	}
	return nil
}

Cognitive complexity: 9, Cyclomatic complexity: 5

Uses: filepath.Clean, strings.Contains.

func (*FileServer) ServeHTTP

func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
	repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)

	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
	}

	filesToHide := fsrv.transformHidePaths(repl)

	root := repl.ReplaceAll(fsrv.Root, ".")
	fsName := repl.ReplaceAll(fsrv.FileSystem, "")

	fileSystem, ok := fsrv.fsmap.Get(fsName)
	if !ok {
		return caddyhttp.Error(http.StatusNotFound, fmt.Errorf("filesystem not found"))
	}

	// remove any trailing `/` as it breaks fs.ValidPath() in the stdlib
	filename := strings.TrimSuffix(caddyhttp.SanitizedPathJoin(root, r.URL.Path), "/")

	if c := fsrv.logger.Check(zapcore.DebugLevel, "sanitized path join"); c != nil {
		c.Write(
			zap.String("site_root", root),
			zap.String("fs", fsName),
			zap.String("request_path", r.URL.Path),
			zap.String("result", filename),
		)
	}

	// get information about the file
	info, err := fs.Stat(fileSystem, filename)
	if err != nil {
		err = fsrv.mapDirOpenError(fileSystem, err, filename)
		if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrInvalid) {
			return fsrv.notFound(w, r, next)
		} else if errors.Is(err, fs.ErrPermission) {
			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
	var implicitIndexFile bool
	if info.IsDir() && len(fsrv.IndexNames) > 0 {
		for _, indexPage := range fsrv.IndexNames {
			indexPage := repl.ReplaceAll(indexPage, "")
			indexPath := caddyhttp.SanitizedPathJoin(filename, indexPage)
			if fileHidden(indexPath, filesToHide) {
				// pretend this file doesn't exist
				if c := fsrv.logger.Check(zapcore.DebugLevel, "hiding index file"); c != nil {
					c.Write(
						zap.String("filename", indexPath),
						zap.Strings("files_to_hide", filesToHide),
					)
				}
				continue
			}

			indexInfo, err := fs.Stat(fileSystem, indexPath)
			if err != nil {
				continue
			}

			// 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

			// we've chosen to use this index file,
			// so replace the last file info and path
			// with that of the index file
			info = indexInfo
			filename = indexPath
			implicitIndexFile = true
			if c := fsrv.logger.Check(zapcore.DebugLevel, "located index file"); c != nil {
				c.Write(zap.String("filename", filename))
			}
			break
		}
	}

	// if still referencing a directory, delegate
	// to browse or return an error
	if info.IsDir() {
		if c := fsrv.logger.Check(zapcore.DebugLevel, "no index file in directory"); c != nil {
			c.Write(
				zap.String("path", filename),
				zap.Strings("index_filenames", fsrv.IndexNames),
			)
		}
		if fsrv.Browse != nil && !fileHidden(filename, filesToHide) {
			return fsrv.serveBrowse(fileSystem, root, filename, w, r, next)
		}
		return fsrv.notFound(w, r, next)
	}

	// 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) {
		if c := fsrv.logger.Check(zapcore.DebugLevel, "hiding file"); c != nil {
			c.Write(
				zap.String("filename", filename),
				zap.Strings("files_to_hide", filesToHide),
			)
		}
		return fsrv.notFound(w, r, next)
	}

	// 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 {
		// 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 + "/"
				if c := fsrv.logger.Check(zapcore.DebugLevel, "redirecting to canonical URI (adding trailing slash for directory"); c != nil {
					c.Write(
						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]
				if c := fsrv.logger.Check(zapcore.DebugLevel, "redirecting to canonical URI (removing trailing slash for file"); c != nil {
					c.Write(
						zap.String("from_path", origReq.URL.Path),
						zap.String("to_path", to),
					)
				}
				return redirect(w, r, to)
			}
		}
	}

	var file fs.File
	respHeader := w.Header()

	// etag is usually unset, but if the user knows what they're doing, let them override it
	etag := respHeader.Get("Etag")

	// static file responses are often compressed, either on-the-fly
	// or with precompressed sidecar files; in any case, the headers
	// should contain "Vary: Accept-Encoding" even when not compressed
	// so caches can craft a reliable key (according to REDbot results)
	// see #5849
	respHeader.Add("Vary", "Accept-Encoding")

	// check for precompressed files
	for _, ae := range encode.AcceptedEncodings(r, fsrv.PrecompressedOrder) {
		precompress, ok := fsrv.precompressors[ae]
		if !ok {
			continue
		}
		compressedFilename := filename + precompress.Suffix()
		compressedInfo, err := fs.Stat(fileSystem, compressedFilename)
		if err != nil || compressedInfo.IsDir() {
			if c := fsrv.logger.Check(zapcore.DebugLevel, "precompressed file not accessible"); c != nil {
				c.Write(zap.String("filename", compressedFilename), zap.Error(err))
			}
			continue
		}
		if c := fsrv.logger.Check(zapcore.DebugLevel, "opening compressed sidecar file"); c != nil {
			c.Write(zap.String("filename", compressedFilename), zap.Error(err))
		}
		file, err = fsrv.openFile(fileSystem, compressedFilename, w)
		if err != nil {
			if c := fsrv.logger.Check(zapcore.WarnLevel, "opening precompressed file failed"); c != nil {
				c.Write(zap.String("filename", compressedFilename), zap.Error(err))
			}
			if caddyErr, ok := err.(caddyhttp.HandlerError); ok && caddyErr.StatusCode == http.StatusServiceUnavailable {
				return err
			}
			file = nil
			continue
		}
		defer file.Close()
		respHeader.Set("Content-Encoding", ae)
		respHeader.Del("Accept-Ranges")

		// try to get the etag from pre computed files if an etag suffix list was provided
		if etag == "" && fsrv.EtagFileExtensions != nil {
			etag, err = fsrv.getEtagFromFile(fileSystem, compressedFilename)
			if err != nil {
				return err
			}
		}

		// 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
		if etag == "" {
			etag = calculateEtag(compressedInfo)
		}

		break
	}

	// no precompressed file found, use the actual file
	if file == nil {
		if c := fsrv.logger.Check(zapcore.DebugLevel, "opening file"); c != nil {
			c.Write(zap.String("filename", filename))
		}

		// open the file
		file, err = fsrv.openFile(fileSystem, 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
		}
		defer file.Close()
		// try to get the etag from pre computed files if an etag suffix list was provided
		if etag == "" && fsrv.EtagFileExtensions != nil {
			etag, err = fsrv.getEtagFromFile(fileSystem, filename)
			if err != nil {
				return err
			}
		}
		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 {
		// 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 {
			respHeader.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
	if etag != "" {
		respHeader.Set("Etag", etag)
	}

	if respHeader.Get("Content-Type") == "" {
		mtyp := mime.TypeByExtension(filepath.Ext(filename))
		if mtyp == "" {
			// do not allow Go to sniff the content-type; see https://www.youtube.com/watch?v=8t8JYpt0egE
			respHeader["Content-Type"] = nil
		} else {
			respHeader.Set("Content-Type", mtyp)
		}
	}

	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)
	if reqErr, ok := r.Context().Value(caddyhttp.ErrorCtxKey).(error); ok {
		statusCodeOverride = http.StatusInternalServerError
		if handlerErr, ok := reqErr.(caddyhttp.HandlerError); ok {
			if handlerErr.StatusCode > 0 {
				statusCodeOverride = handlerErr.StatusCode
			}
		}
	}

	// if a status code override is configured, run the replacer on it
	if codeStr := fsrv.StatusCode.String(); codeStr != "" {
		statusCodeOverride, err = strconv.Atoi(repl.ReplaceAll(codeStr, ""))
		if err != nil {
			return caddyhttp.Error(http.StatusInternalServerError, err)
		}
	}

	// 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}
	}

	// 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 there
	// are rare)
	http.ServeContent(w, r, info.Name(), info.ModTime(), file.(io.ReadSeeker))

	return nil
}

Cognitive complexity: 113, Cyclomatic complexity: 68

Uses: caddyhttp.Error, caddyhttp.ErrorCtxKey, caddyhttp.HandlerError, caddyhttp.OriginalRequestCtxKey, caddyhttp.SanitizedPathJoin, encode.AcceptedEncodings, errors.Is, filepath.Ext, fmt.Errorf, fs.ErrInvalid, fs.ErrNotExist, fs.ErrPermission, fs.File, fs.Stat, http.MethodGet, http.MethodHead, http.Request, http.ServeContent, http.StatusBadRequest, http.StatusForbidden, http.StatusInternalServerError, http.StatusMethodNotAllowed, http.StatusNotFound, http.StatusServiceUnavailable, io.ReadSeeker, mime.TypeByExtension, path.Base, runtime.GOOS, strconv.Atoi, strings.Contains, strings.HasSuffix, strings.TrimRight, strings.TrimSuffix, zap.Error, zap.String, zap.Strings, zapcore.DebugLevel, zapcore.WarnLevel.

func (*FileServer) UnmarshalCaddyfile

UnmarshalCaddyfile parses the file_server directive. It enables the static file server and configures it with this syntax:

file_server [<matcher>] [browse] {
    fs            <filesystem>
    root          <path>
    hide          <files...>
    index         <files...>
    browse        [<template_file>]
    precompressed <formats...>
    status        <status>
    disable_canonical_uris
}

The FinalizeUnmarshalCaddyfile method should be called after this to finalize setup of hidden Caddyfiles.

func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
	d.Next()	// consume directive name

	args := d.RemainingArgs()
	switch len(args) {
	case 0:
	case 1:
		if args[0] != "browse" {
			return d.ArgErr()
		}
		fsrv.Browse = new(Browse)
	default:
		return d.ArgErr()
	}

	for nesting := d.Nesting(); d.NextBlock(nesting); {
		switch d.Val() {
		case "fs":
			if !d.NextArg() {
				return d.ArgErr()
			}
			if fsrv.FileSystem != "" {
				return d.Err("file system already specified")
			}
			fsrv.FileSystem = d.Val()

		case "hide":
			fsrv.Hide = d.RemainingArgs()
			if len(fsrv.Hide) == 0 {
				return d.ArgErr()
			}

		case "index":
			fsrv.IndexNames = d.RemainingArgs()
			if len(fsrv.IndexNames) == 0 {
				return d.ArgErr()
			}

		case "root":
			if !d.Args(&fsrv.Root) {
				return d.ArgErr()
			}

		case "browse":
			if fsrv.Browse != nil {
				return d.Err("browsing is already configured")
			}
			fsrv.Browse = new(Browse)
			d.Args(&fsrv.Browse.TemplateFile)
			for nesting := d.Nesting(); d.NextBlock(nesting); {
				switch d.Val() {
				case "reveal_symlinks":
					if fsrv.Browse.RevealSymlinks {
						return d.Err("Symlinks path reveal is already enabled")
					}
					fsrv.Browse.RevealSymlinks = true
				case "sort":
					for d.NextArg() {
						dVal := d.Val()
						switch dVal {
						case sortByName, sortByNameDirFirst, sortBySize, sortByTime, sortOrderAsc, sortOrderDesc:
							fsrv.Browse.SortOptions = append(fsrv.Browse.SortOptions, dVal)
						default:
							return d.Errf("unknown sort option '%s'", dVal)
						}
					}
				case "file_limit":
					fileLimit := d.RemainingArgs()
					if len(fileLimit) != 1 {
						return d.Err("file_limit should have an integer value")
					}
					val, _ := strconv.Atoi(fileLimit[0])
					if fsrv.Browse.FileLimit != 0 {
						return d.Err("file_limit is already enabled")
					}
					fsrv.Browse.FileLimit = val
				default:
					return d.Errf("unknown subdirective '%s'", d.Val())
				}
			}

		case "precompressed":
			fsrv.PrecompressedOrder = d.RemainingArgs()
			if len(fsrv.PrecompressedOrder) == 0 {
				fsrv.PrecompressedOrder = []string{"br", "zstd", "gzip"}
			}

			for _, format := range fsrv.PrecompressedOrder {
				modID := "http.precompressed." + format
				mod, err := caddy.GetModule(modID)
				if err != nil {
					return d.Errf("getting module named '%s': %v", modID, err)
				}
				inst := mod.New()
				precompress, ok := inst.(encode.Precompressed)
				if !ok {
					return d.Errf("module %s is not a precompressor; is %T", modID, inst)
				}
				if fsrv.PrecompressedRaw == nil {
					fsrv.PrecompressedRaw = make(caddy.ModuleMap)
				}
				fsrv.PrecompressedRaw[format] = caddyconfig.JSON(precompress, nil)
			}

		case "status":
			if !d.NextArg() {
				return d.ArgErr()
			}
			fsrv.StatusCode = caddyhttp.WeakString(d.Val())

		case "disable_canonical_uris":
			if d.NextArg() {
				return d.ArgErr()
			}
			falseBool := false
			fsrv.CanonicalURIs = &falseBool

		case "pass_thru":
			if d.NextArg() {
				return d.ArgErr()
			}
			fsrv.PassThru = true

		case "etag_file_extensions":
			etagFileExtensions := d.RemainingArgs()
			if len(etagFileExtensions) == 0 {
				return d.ArgErr()
			}
			fsrv.EtagFileExtensions = etagFileExtensions

		default:
			return d.Errf("unknown subdirective '%s'", d.Val())
		}
	}

	return nil
}

Cognitive complexity: 70, Cyclomatic complexity: 43

Uses: caddyconfig.JSON, caddyhttp.WeakString, encode.Precompressed, strconv.Atoi.

func (*MatchFile) Provision

Provision sets up m's defaults.

func (m *MatchFile) Provision(ctx caddy.Context) error {
	m.logger = ctx.Logger()

	m.fsmap = ctx.Filesystems()

	if m.Root == "" {
		m.Root = "{http.vars.root}"
	}

	if m.FileSystem == "" {
		m.FileSystem = "{http.vars.fs}"
	}

	// if list of files to try was omitted entirely, assume URL path
	// (use placeholder instead of r.URL.Path; see issue #4146)
	if m.TryFiles == nil {
		m.TryFiles = []string{"{http.request.uri.path}"}
	}
	return nil
}

Cognitive complexity: 7, Cyclomatic complexity: 4

func (MatchFile) CELLibrary

CELLibrary produces options that expose this matcher for use in CEL expression matchers.

Example:

expression file()
expression file({http.request.uri.path}, '/index.php')
expression file({'root': '/srv', 'try_files': [{http.request.uri.path}, '/index.php'], 'try_policy': 'first_exist', 'split_path': ['.php']})

func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) {
	requestType := cel.ObjectType("http.Request")

	matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcherWithError, error) {
		values, err := caddyhttp.CELValueToMapStrList(data)
		if err != nil {
			return nil, err
		}

		var root string
		if len(values["root"]) > 0 {
			root = values["root"][0]
		}

		var fsName string
		if len(values["fs"]) > 0 {
			fsName = values["fs"][0]
		}

		var try_policy string
		if len(values["try_policy"]) > 0 {
			try_policy = values["try_policy"][0]
		}

		m := MatchFile{
			Root:		root,
			TryFiles:	values["try_files"],
			TryPolicy:	try_policy,
			SplitPath:	values["split_path"],
			FileSystem:	fsName,
		}

		err = m.Provision(ctx)
		return m, err
	}

	envOptions := []cel.EnvOption{
		cel.Macros(parser.NewGlobalVarArgMacro("file", celFileMatcherMacroExpander())),
		cel.Function("file", cel.Overload("file_request_map", []*cel.Type{requestType, caddyhttp.CELTypeJSON}, cel.BoolType)),
		cel.Function("file_request_map",
			cel.Overload("file_request_map", []*cel.Type{requestType, caddyhttp.CELTypeJSON}, cel.BoolType),
			cel.SingletonBinaryBinding(caddyhttp.CELMatcherRuntimeFunction("file_request_map", matcherFactory))),
	}

	programOptions := []cel.ProgramOption{
		cel.CustomDecorator(caddyhttp.CELMatcherDecorator("file_request_map", matcherFactory)),
	}

	return caddyhttp.NewMatcherCELLibrary(envOptions, programOptions), nil
}

Cognitive complexity: 14, Cyclomatic complexity: 5

Uses: caddyhttp.CELMatcherDecorator, caddyhttp.CELMatcherRuntimeFunction, caddyhttp.CELTypeJSON, caddyhttp.CELValueToMapStrList, caddyhttp.NewMatcherCELLibrary, caddyhttp.RequestMatcherWithError, cel.BoolType, cel.CustomDecorator, cel.EnvOption, cel.Function, cel.Macros, cel.ObjectType, cel.Overload, cel.ProgramOption, cel.SingletonBinaryBinding, cel.Type, parser.NewGlobalVarArgMacro, ref.Val.

func (MatchFile) CaddyModule

CaddyModule returns the Caddy module information.

func (MatchFile) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:	"http.matchers.file",
		New:	func() caddy.Module { return new(MatchFile) },
	}
}

Cognitive complexity: 2, Cyclomatic complexity: 1

func (MatchFile) Match

Match returns true if r matches m. Returns true if a file was matched. If so, four placeholders will be available:

  • http.matchers.file.relative: Path to file relative to site root
  • http.matchers.file.absolute: Path to file including site root
  • http.matchers.file.type: file or directory
  • http.matchers.file.remainder: Portion remaining after splitting file path (if configured)

func (m MatchFile) Match(r *http.Request) bool {
	match, err := m.selectFile(r)
	if err != nil {
		// nolint:staticcheck
		caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err)
	}
	return match
}

Cognitive complexity: 2, Cyclomatic complexity: 2

Uses: caddyhttp.MatcherErrorVarKey, caddyhttp.SetVar.

func (MatchFile) MatchWithError

MatchWithError returns true if r matches m.

func (m MatchFile) MatchWithError(r *http.Request) (bool, error) {
	return m.selectFile(r)
}

Cognitive complexity: 0, Cyclomatic complexity: 1

func (MatchFile) Validate

Validate ensures m has a valid configuration.

func (m MatchFile) Validate() error {
	switch m.TryPolicy {
	case "",
		tryPolicyFirstExist,
		tryPolicyFirstExistFallback,
		tryPolicyLargestSize,
		tryPolicySmallestSize,
		tryPolicyMostRecentlyMod:
	default:
		return fmt.Errorf("unknown try policy %s", m.TryPolicy)
	}
	return nil
}

Cognitive complexity: 3, Cyclomatic complexity: 3

Uses: fmt.Errorf.

Breadcrumbs returns l.Path where every element maps the link to the text to display.

func (l browseTemplateContext) Breadcrumbs() []crumb {
	if len(l.Path) == 0 {
		return []crumb{}
	}

	// skip trailing slash
	lpath := l.Path
	if lpath[len(lpath)-1] == '/' {
		lpath = lpath[:len(lpath)-1]
	}
	parts := strings.Split(lpath, "/")
	result := make([]crumb, len(parts))
	for i, p := range parts {
		if i == 0 && p == "" {
			p = "/"
		}
		// the directory name could include an encoded slash in its path,
		// so the item name should be unescaped in the loop rather than unescaping the
		// entire path outside the loop.
		p, _ = url.PathUnescape(p)
		lnk := strings.Repeat("../", len(parts)-i-1)
		result[i] = crumb{Link: lnk, Text: p}
	}

	return result
}

Cognitive complexity: 11, Cyclomatic complexity: 6

Uses: strings.Repeat, strings.Split, url.PathUnescape.

func (browseTemplateContext) HumanTotalFileSize

HumanTotalFileSize returns the total size of all files in the listing as a human-readable string in IEC format (i.e. power of 2 or base 1024).

func (btc browseTemplateContext) HumanTotalFileSize() string {
	return humanize.IBytes(uint64(btc.TotalFileSize))
}

Cognitive complexity: 0, Cyclomatic complexity: 1

HumanTotalFileSizeFollowingSymlinks is the same as HumanTotalFileSize except the returned value reflects the size of symlink targets.

func (btc browseTemplateContext) HumanTotalFileSizeFollowingSymlinks() string {
	return humanize.IBytes(uint64(btc.TotalFileSizeFollowingSymlinks))
}

Cognitive complexity: 0, Cyclomatic complexity: 1

func (byName) Len

func (l byName) Len() int	{ return len(l.Items) }

Cognitive complexity: 0, Cyclomatic complexity: 1

func (byName) Less

func (l byName) Less(i, j int) bool {
	return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
}

Cognitive complexity: 0, Cyclomatic complexity: 1

Uses: strings.ToLower.

func (byName) Swap

func (l byName) Swap(i, j int)	{ l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }

Cognitive complexity: 0, Cyclomatic complexity: 1

func (fileInfo) HasExt

HasExt returns true if the filename has any of the given suffixes, case-insensitive.

func (fi fileInfo) HasExt(exts ...string) bool {
	return slices.ContainsFunc(exts, func(ext string) bool {
		return strings.HasSuffix(strings.ToLower(fi.Name), strings.ToLower(ext))
	})
}

Cognitive complexity: 1, Cyclomatic complexity: 1

Uses: slices.ContainsFunc, strings.HasSuffix, strings.ToLower.

func (fileInfo) HumanModTime

HumanModTime returns the modified time of the file as a human-readable string given by format.

func (fi fileInfo) HumanModTime(format string) string {
	return fi.ModTime.Format(format)
}

Cognitive complexity: 0, Cyclomatic complexity: 1

func (fileInfo) HumanSize

HumanSize returns the size of the file as a human-readable string in IEC format (i.e. power of 2 or base 1024).

func (fi fileInfo) HumanSize() string {
	return humanize.IBytes(uint64(fi.Size))
}

Cognitive complexity: 0, Cyclomatic complexity: 1

func (statusOverrideResponseWriter) Unwrap

Unwrap returns the underlying ResponseWriter, necessary for http.ResponseController to work correctly.

func (wr statusOverrideResponseWriter) Unwrap() http.ResponseWriter {
	return wr.ResponseWriter
}

Cognitive complexity: 0, Cyclomatic complexity: 1

func (statusOverrideResponseWriter) WriteHeader

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)
}

Cognitive complexity: 0, Cyclomatic complexity: 1

Private functions

func calculateEtag

calculateEtag computes an entity tag using a strong validator without consuming the contents of the file. It requires the file info contain the correct size and modification time. It strives to implement the semantics regarding ETags as defined by RFC 9110 section 8.8.3 and 8.8.1. See https://www.rfc-editor.org/rfc/rfc9110.html#section-8.8.3.

As our implementation uses file modification timestamp and size, note the following from RFC 9110 section 8.8.1: "A representation's modification time, if defined with only one-second resolution, might be a weak validator if it is possible for the representation to be modified twice during a single second and retrieved between those modifications." The ext4 file system, which underpins the vast majority of Caddy deployments, stores mod times with millisecond precision, which we consider precise enough to qualify as a strong validator.

calculateEtag (d os.FileInfo) string
References: strconv.FormatInt, strings.Builder.

func celFileMatcherMacroExpander

celFileMatcherMacroExpander () parser.MacroExpander
References: ast.Expr, caddyhttp.CELRequestVarName, common.Error, parser.ExprHelper, types.String.

func cmdFileServer

cmdFileServer (fs caddycmd.Flags) (int, error)
References: caddyconfig.JSON, caddyconfig.JSONModuleObject, caddyhttp.App, caddyhttp.MatchHost, caddyhttp.Route, caddyhttp.RouteList, caddyhttp.Server, caddyhttp.ServerLogConfig, caddytpl.Templates, certmagic.HTTPSPort, encode.Encode, encode.Precompressed, fmt.Errorf, json.RawMessage, log.Printf, strconv.Itoa, time.Second, zap.DebugLevel.

func fileHidden

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).

fileHidden (filename string, hide []string) bool
References: filepath.Match, strings.Contains, strings.HasPrefix, strings.Split, strings.TrimPrefix.

func indexFold

There is no strings.IndexFold() function like there is strings.EqualFold(), but we can use strings.EqualFold() to build our own case-insensitive substring search (as of Go 1.14).

indexFold (haystack,needle string) int
References: strings.EqualFold.

func init

init ()
References: httpcaddyfile.RegisterDirective, httpcaddyfile.RegisterHandlerDirective.

func isCELCaddyPlaceholderCall

isCELCaddyPlaceholderCall returns whether the expression is a caddy placeholder call.

isCELCaddyPlaceholderCall (e ast.Expr) bool
References: ast.CallKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind, ast.UnspecifiedExprKind, caddyhttp.CELPlaceholderFuncName.

func isCELConcatCall

isCELConcatCall tests whether the expression is a concat function (+) with string, placeholder, or other concat call arguments.

isCELConcatCall (e ast.Expr) bool
References: ast.CallKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind, ast.UnspecifiedExprKind, operators.Add.

func isCELStringExpr

isCELStringExpr indicates whether the expression is a supported string expression

isCELStringExpr (e ast.Expr) bool

func isCELStringListLiteral

isCELStringListLiteral returns whether the expression resolves to a list literal containing only string constants or a placeholder call.

isCELStringListLiteral (e ast.Expr) bool
References: ast.CallKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind, ast.UnspecifiedExprKind.

func isCELStringLiteral

isCELStringLiteral returns whether the expression is a CEL string literal.

isCELStringLiteral (e ast.Expr) bool
References: ast.CallKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind, ast.UnspecifiedExprKind, types.StringType.

func isCELTryFilesLiteral

isCELTryFilesLiteral returns whether the expression resolves to a map literal containing only string keys with or a placeholder call.

isCELTryFilesLiteral (e ast.Expr) bool
References: ast.CallKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind, ast.UnspecifiedExprKind, types.StringType.

isSymlink return true if f is a symbolic link.

isSymlink (f fs.FileInfo) bool
References: os.ModeSymlink.

func parseCaddyfile

parseCaddyfile parses the file_server directive. See UnmarshalCaddyfile for the syntax.

parseCaddyfile (h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)

func parseErrorCode

parseErrorCode checks if the input is a status code number, prefixed by "=", and returns an error if so.

parseErrorCode (input string) error
References: caddyhttp.Error, fmt.Errorf, strconv.Atoi.

func parseTryFiles

parseTryFiles parses the try_files directive. It combines a file matcher with a rewrite directive, so this is not a standard handler directive. A try_files directive has this syntax (notice no matcher tokens accepted):

try_files <files...> {
	policy first_exist|smallest_size|largest_size|most_recently_modified
}

and is basically shorthand for:

@try_files file {
	try_files <files...>
	policy first_exist|smallest_size|largest_size|most_recently_modified
}
rewrite @try_files {http.matchers.file.relative}

This directive rewrites request paths only, preserving any other part of the URI, unless the part is explicitly given in the file list. For example, if any of the files in the list have a query string:

try_files {path} index.php?{query}&p={path}

then the query string will not be treated as part of the file name; and if that file matches, the given query string will replace any query string that already exists on the request URI.

parseTryFiles (h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
References: httpcaddyfile.ConfigValue, rewrite.Rewrite, strings.Index.

func redirect

redirect performs a redirect to a given path. The 'toPath' parameter MUST be solely a path, and MUST NOT include a query.

redirect (w http.ResponseWriter, r *http.Request, toPath string) error
References: http.Redirect, http.StatusPermanentRedirect, strings.HasPrefix, strings.TrimPrefix.

func browseApplyQueryParams

browseApplyQueryParams applies query parameters to the listing. It mutates the listing and may set cookies.

browseApplyQueryParams (w http.ResponseWriter, r *http.Request, listing *browseTemplateContext)
References: http.Cookie, http.SetCookie.

func directoryListing

directoryListing (ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, canGoUp bool, root,urlPath string, repl *caddy.Replacer) *browseTemplateContext
References: caddyhttp.SanitizedPathJoin, filepath.EvalSymlinks, fs.Stat, path.Base, path.Join, url.PathUnescape, url.URL, zap.String, zapcore.ErrorLevel.

func getEtagFromFile

Finds the first corresponding etag file for a given file in the file system and return its content

getEtagFromFile (fileSystem fs.FS, filename string) (string, error)
References: bytes.ReplaceAll, errors.Is, fmt.Errorf, fs.ErrNotExist, fs.ReadFile.

func isSymlinkTargetDir

isSymlinkTargetDir returns true if f's symbolic link target is a directory.

isSymlinkTargetDir (fileSystem fs.FS, f fs.FileInfo, root,urlPath string) bool
References: caddyhttp.SanitizedPathJoin, fs.Stat, path.Join.

func loadDirectoryContents

loadDirectoryContents (ctx context.Context, fileSystem fs.FS, dir fs.ReadDirFile, root,urlPath string, repl *caddy.Replacer) (*browseTemplateContext, error)
References: io.EOF.

func makeBrowseTemplate

makeBrowseTemplate creates the template to be used for directory listings.

makeBrowseTemplate (tplCtx *templateContext) (*template.Template, error)
References: fmt.Errorf, path.Base, template.Template.

func mapDirOpenError

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/

mapDirOpenError (fileSystem fs.FS, originalErr error, name string) error
References: errors.Is, fs.ErrNotExist, fs.ErrPermission, fs.Stat, strings.Join, strings.Split.

func notFound

notFound returns a 404 error or, if pass-thru is enabled, it calls the next handler in the chain.

notFound (w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error
References: caddyhttp.Error, http.StatusNotFound.

func openFile

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).

openFile (fileSystem fs.FS, filename string, w http.ResponseWriter) (fs.File, error)
References: caddyhttp.Error, errors.Is, fs.ErrNotExist, fs.ErrPermission, http.StatusForbidden, http.StatusNotFound, http.StatusServiceUnavailable, strconv.Itoa, weakrand.Intn, zap.Error, zap.Int, zap.String, zapcore.DebugLevel.

func serveBrowse

serveBrowse (fileSystem fs.FS, root,dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error
References: bytes.Buffer, caddyhttp.Error, caddyhttp.OriginalRequestCtxKey, errors.Is, fmt.Errorf, fmt.Fprintf, fmt.Fprintln, fs.ErrNotExist, fs.ErrPermission, fs.ReadDirFile, http.Dir, http.FileSystem, http.Request, http.StatusForbidden, http.StatusInternalServerError, http.StatusNotModified, http.TimeFormat, json.NewEncoder, path.Base, path.Clean, strings.Contains, strings.HasSuffix, strings.Join, strings.ToLower, tabwriter.AlignRight, tabwriter.NewWriter, templates.TemplateContext, templates.WrappedHeader, time.Local, time.ParseInLocation, time.Second, zap.String, zapcore.DebugLevel.

func transformHidePaths

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.

transformHidePaths (repl *caddy.Replacer) []string
References: strings.Contains.

func applySortAndLimit

applySortAndLimit (sortParam,orderParam,limitParam string, offsetParam string)
References: sort.Reverse, sort.Sort, strconv.Atoi.

func firstSplit

firstSplit returns the first result where the path can be split in two by a value in m.SplitPath. The return values are the first piece of the path that ends with the split substring and the remainder. If the path cannot be split, the path is returned as-is (with no remainder).

firstSplit (path string) string
References: strings.HasPrefix.

func selectFile

selectFile chooses a file according to m.TryPolicy by appending the paths in m.TryFiles to m.Root, with placeholder replacements.

selectFile (r *http.Request) (bool, error)
References: caddyhttp.SanitizedPathJoin, filepath.Clean, filepath.ToSlash, fmt.Stringer, fs.Glob, fs.Stat, os.FileInfo, path.Clean, runtime.GOOS, strings.HasSuffix, strings.TrimPrefix, zap.Error, zap.String, zapcore.ErrorLevel.

func strictFileExists

strictFileExists returns true if file exists and matches the convention of the given file path. If the path ends in a forward slash, the file must also be a directory; if it does NOT end in a forward slash, the file must NOT be a directory.

strictFileExists (fileSystem fs.FS, file string) (os.FileInfo, bool)
References: fs.Stat, strings.HasSuffix.


Tests

Files: 3. Third party imports: 0. Imports from organisation: 0. Tests: 6. Benchmarks: 0.

Vars

var expressionTests = []struct {
	name			string
	expression		*caddyhttp.MatchExpression
	urlTarget		string
	httpMethod		string
	httpHeader		*http.Header
	wantErr			bool
	wantResult		bool
	clientCertificate	[]byte
	expectedPath		string
}{
	{
		name:	"file error no args (MatchFile)",
		expression: &caddyhttp.MatchExpression{
			Expr: `file()`,
		},
		urlTarget:	"https://example.com/foo.txt",
		wantResult:	true,
	},
	{
		name:	"file error bad try files (MatchFile)",
		expression: &caddyhttp.MatchExpression{
			Expr: `file({"try_file": ["bad_arg"]})`,
		},
		urlTarget:	"https://example.com/foo",
		wantErr:	true,
	},
	{
		name:	"file match short pattern index.php (MatchFile)",
		expression: &caddyhttp.MatchExpression{
			Expr: `file("index.php")`,
		},
		urlTarget:	"https://example.com/foo",
		wantResult:	true,
	},
	{
		name:	"file match short pattern foo.txt (MatchFile)",
		expression: &caddyhttp.MatchExpression{
			Expr: `file({http.request.uri.path})`,
		},
		urlTarget:	"https://example.com/foo.txt",
		wantResult:	true,
	},
	{
		name:	"file match index.php (MatchFile)",
		expression: &caddyhttp.MatchExpression{
			Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}, "/index.php"]})`,
		},
		urlTarget:	"https://example.com/foo",
		wantResult:	true,
	},
	{
		name:	"file match long pattern foo.txt (MatchFile)",
		expression: &caddyhttp.MatchExpression{
			Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`,
		},
		urlTarget:	"https://example.com/foo.txt",
		wantResult:	true,
	},
	{
		name:	"file match long pattern foo.txt with concatenation (MatchFile)",
		expression: &caddyhttp.MatchExpression{
			Expr: `file({"root": ".", "try_files": ["./testdata" + {http.request.uri.path}]})`,
		},
		urlTarget:	"https://example.com/foo.txt",
		wantResult:	true,
	},
	{
		name:	"file not match long pattern (MatchFile)",
		expression: &caddyhttp.MatchExpression{
			Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`,
		},
		urlTarget:	"https://example.com/nopenope.txt",
		wantResult:	false,
	},
	{
		name:	"file match long pattern foo.txt with try_policy (MatchFile)",
		expression: &caddyhttp.MatchExpression{
			Expr: `file({"root": "./testdata", "try_policy": "largest_size", "try_files": ["foo.txt", "large.txt"]})`,
		},
		urlTarget:	"https://example.com/",
		wantResult:	true,
		expectedPath:	"/large.txt",
	},
}

Test functions

TestBreadcrumbs

TestFileHidden

References: filepath.Abs, filepath.FromSlash, runtime.GOOS, strings.HasPrefix.

TestFileMatcher

References: caddyhttp.NewTestReplacer, filesystems.FilesystemMap, http.Request, os.Create, os.Remove, runtime.GOOS, url.Parse, url.PathEscape.

TestFirstSplit

References: filesystems.FilesystemMap.

TestMatchExpressionMatch

References: caddyhttp.NewTestReplacer, context.Background, context.WithValue, httptest.NewRequest, testing.T.

TestPHPFileMatcher

References: caddyhttp.NewTestReplacer, filesystems.FilesystemMap, http.Request, url.Parse.