mirror of
https://github.com/caddyserver/caddy.git
synced 2026-08-27 04:07:28 +00:00
Add error & subroute handlers; weakString; other minor handler changes
This commit is contained in:
parent
4a3a418156
commit
eb8625f774
10 changed files with 331 additions and 212 deletions
|
|
@ -15,55 +15,166 @@
|
|||
package fileserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(caddy.Module{
|
||||
Name: "http.matchers.file",
|
||||
New: func() interface{} { return new(FileMatcher) },
|
||||
New: func() interface{} { return new(MatchFile) },
|
||||
})
|
||||
}
|
||||
|
||||
// FileMatcher is a matcher that can match requests
|
||||
// based on the local file system.
|
||||
// TODO: Not sure how to do this well; we'd need the ability to
|
||||
// hide files, etc...
|
||||
// TODO: Also consider a feature to match directory that
|
||||
// contains a certain filename (use filepath.Glob), useful
|
||||
// if wanting to map directory-URI requests where the dir
|
||||
// has index.php to PHP backends, for example (although this
|
||||
// can effectively be done with rehandling already)
|
||||
type FileMatcher struct {
|
||||
Root string `json:"root"`
|
||||
Path string `json:"path"`
|
||||
Flags []string `json:"flags"`
|
||||
// MatchFile is an HTTP request matcher that can match
|
||||
// requests based upon file existence.
|
||||
type MatchFile struct {
|
||||
// The root directory, used for creating absolute
|
||||
// file paths, and required when working with
|
||||
// relative paths; if not specified, the current
|
||||
// directory is assumed. Accepts placeholders.
|
||||
Root string `json:"root,omitempty"`
|
||||
|
||||
// The list of files to try. Each path here is
|
||||
// considered relatice to Root. If nil, the
|
||||
// request URL's path will be assumed. Accepts
|
||||
// placeholders.
|
||||
TryFiles []string `json:"try_files,omitempty"`
|
||||
|
||||
// How to choose a file in TryFiles.
|
||||
// Default is first_exist.
|
||||
TryPolicy string `json:"try_policy,omitempty"`
|
||||
}
|
||||
|
||||
// Match matches the request r against m.
|
||||
func (m FileMatcher) Match(r *http.Request) bool {
|
||||
fullPath := sanitizedPathJoin(m.Root, m.Path)
|
||||
var match bool
|
||||
if len(m.Flags) > 0 {
|
||||
match = true
|
||||
fi, err := os.Stat(fullPath)
|
||||
for _, f := range m.Flags {
|
||||
switch f {
|
||||
case "EXIST":
|
||||
match = match && os.IsNotExist(err)
|
||||
case "DIR":
|
||||
match = match && err == nil && fi.IsDir()
|
||||
default:
|
||||
match = false
|
||||
// Validate ensures m has a valid configuration.
|
||||
func (m MatchFile) Validate() error {
|
||||
switch m.TryPolicy {
|
||||
case "",
|
||||
tryPolicyFirstExist,
|
||||
tryPolicyLargestSize,
|
||||
tryPolicySmallestSize,
|
||||
tryPolicyMostRecentMod:
|
||||
default:
|
||||
return fmt.Errorf("unknown try policy %s", m.TryPolicy)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Match returns true if r matches m. Returns true
|
||||
// if a file was matched. If so, two placeholders
|
||||
// will be available:
|
||||
// - http.matchers.file.relative
|
||||
// - http.matchers.file.absolute
|
||||
func (m MatchFile) Match(r *http.Request) bool {
|
||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
|
||||
rel, abs, matched := m.selectFile(r)
|
||||
if matched {
|
||||
repl.Set("http.matchers.file.relative", rel)
|
||||
repl.Set("http.matchers.file.absolute", abs)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// selectFile chooses a file according to m.TryPolicy by appending
|
||||
// the paths in m.TryFiles to m.Root, with placeholder replacements.
|
||||
// It returns the root-relative path to the matched file, the full
|
||||
// or absolute path, and whether a match was made.
|
||||
func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) {
|
||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
|
||||
|
||||
root := repl.ReplaceAll(m.Root, "")
|
||||
|
||||
// if list of files to try was omitted entirely,
|
||||
// assume URL path
|
||||
if m.TryFiles == nil {
|
||||
// m is not a pointer, so this is safe
|
||||
m.TryFiles = []string{r.URL.Path}
|
||||
}
|
||||
|
||||
switch m.TryPolicy {
|
||||
case "", tryPolicyFirstExist:
|
||||
for _, f := range m.TryFiles {
|
||||
suffix := repl.ReplaceAll(f, "")
|
||||
fullpath := sanitizedPathJoin(root, suffix)
|
||||
if fileExists(fullpath) {
|
||||
return suffix, fullpath, true
|
||||
}
|
||||
}
|
||||
|
||||
case tryPolicyLargestSize:
|
||||
var largestSize int64
|
||||
var largestFilename string
|
||||
var largestSuffix string
|
||||
for _, f := range m.TryFiles {
|
||||
suffix := repl.ReplaceAll(f, "")
|
||||
fullpath := sanitizedPathJoin(root, suffix)
|
||||
info, err := os.Stat(fullpath)
|
||||
if err == nil && info.Size() > largestSize {
|
||||
largestSize = info.Size()
|
||||
largestFilename = fullpath
|
||||
largestSuffix = suffix
|
||||
}
|
||||
}
|
||||
return largestSuffix, largestFilename, true
|
||||
|
||||
case tryPolicySmallestSize:
|
||||
var smallestSize int64
|
||||
var smallestFilename string
|
||||
var smallestSuffix string
|
||||
for _, f := range m.TryFiles {
|
||||
suffix := repl.ReplaceAll(f, "")
|
||||
fullpath := sanitizedPathJoin(root, suffix)
|
||||
info, err := os.Stat(fullpath)
|
||||
if err == nil && (smallestSize == 0 || info.Size() < smallestSize) {
|
||||
smallestSize = info.Size()
|
||||
smallestFilename = fullpath
|
||||
smallestSuffix = suffix
|
||||
}
|
||||
}
|
||||
return smallestSuffix, smallestFilename, true
|
||||
|
||||
case tryPolicyMostRecentMod:
|
||||
var recentDate time.Time
|
||||
var recentFilename string
|
||||
var recentSuffix string
|
||||
for _, f := range m.TryFiles {
|
||||
suffix := repl.ReplaceAll(f, "")
|
||||
fullpath := sanitizedPathJoin(root, suffix)
|
||||
info, err := os.Stat(fullpath)
|
||||
if err == nil &&
|
||||
(recentDate.IsZero() || info.ModTime().After(recentDate)) {
|
||||
recentDate = info.ModTime()
|
||||
recentFilename = fullpath
|
||||
recentSuffix = suffix
|
||||
}
|
||||
}
|
||||
return recentSuffix, recentFilename, true
|
||||
}
|
||||
return match
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Interface guard
|
||||
var _ caddyhttp.RequestMatcher = (*FileMatcher)(nil)
|
||||
// fileExists returns true if file exists.
|
||||
func fileExists(file string) bool {
|
||||
_, err := os.Stat(file)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
const (
|
||||
tryPolicyFirstExist = "first_exist"
|
||||
tryPolicyLargestSize = "largest_size"
|
||||
tryPolicySmallestSize = "smallest_size"
|
||||
tryPolicyMostRecentMod = "most_recent_modified"
|
||||
)
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddy.Validator = (*MatchFile)(nil)
|
||||
_ caddyhttp.RequestMatcher = (*MatchFile)(nil)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,27 +42,15 @@ func init() {
|
|||
|
||||
// FileServer implements a static file server responder for Caddy.
|
||||
type FileServer struct {
|
||||
Root string `json:"root,omitempty"` // default is current directory
|
||||
Hide []string `json:"hide,omitempty"`
|
||||
IndexNames []string `json:"index_names,omitempty"`
|
||||
Files []string `json:"files,omitempty"` // all relative to the root; default is request URI path
|
||||
SelectionPolicy string `json:"selection_policy,omitempty"`
|
||||
Rehandle bool `json:"rehandle,omitempty"` // issue a rehandle (internal redirect) if request is rewritten
|
||||
Fallback caddyhttp.RouteList `json:"fallback,omitempty"`
|
||||
Browse *Browse `json:"browse,omitempty"`
|
||||
// TODO: Etag
|
||||
Root string `json:"root,omitempty"` // default is current directory
|
||||
Hide []string `json:"hide,omitempty"`
|
||||
IndexNames []string `json:"index_names,omitempty"`
|
||||
Browse *Browse `json:"browse,omitempty"`
|
||||
// TODO: Content negotiation
|
||||
}
|
||||
|
||||
// Provision sets up the static files responder.
|
||||
func (fsrv *FileServer) Provision(ctx caddy.Context) error {
|
||||
if fsrv.Fallback != nil {
|
||||
err := fsrv.Fallback.Provision(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up fallback routes: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if fsrv.IndexNames == nil {
|
||||
fsrv.IndexNames = defaultIndexNames
|
||||
}
|
||||
|
|
@ -87,50 +75,14 @@ func (fsrv *FileServer) Provision(ctx caddy.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
selectionPolicyFirstExisting = "first_existing"
|
||||
selectionPolicyLargestSize = "largest_size"
|
||||
selectionPolicySmallestSize = "smallest_size"
|
||||
selectionPolicyRecentlyMod = "most_recently_modified"
|
||||
)
|
||||
|
||||
// Validate ensures that sf has a valid configuration.
|
||||
func (fsrv *FileServer) Validate() error {
|
||||
switch fsrv.SelectionPolicy {
|
||||
case "",
|
||||
selectionPolicyFirstExisting,
|
||||
selectionPolicyLargestSize,
|
||||
selectionPolicySmallestSize,
|
||||
selectionPolicyRecentlyMod:
|
||||
default:
|
||||
return fmt.Errorf("unknown selection policy %s", fsrv.SelectionPolicy)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
|
||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
|
||||
|
||||
filesToHide := fsrv.transformHidePaths(repl)
|
||||
|
||||
// map the request to a filename
|
||||
pathBefore := r.URL.Path
|
||||
filename := fsrv.selectFile(r, repl, filesToHide)
|
||||
if filename == "" {
|
||||
// no files worked, so resort to fallback
|
||||
if fsrv.Fallback != nil {
|
||||
fallback := fsrv.Fallback.BuildCompositeRoute(w, r)
|
||||
return fallback.ServeHTTP(w, r)
|
||||
}
|
||||
return caddyhttp.Error(http.StatusNotFound, nil)
|
||||
}
|
||||
|
||||
// if the ultimate destination has changed, submit
|
||||
// this request for a rehandling (internal redirect)
|
||||
// if configured to do so
|
||||
if r.URL.Path != pathBefore && fsrv.Rehandle {
|
||||
return caddyhttp.ErrRehandle
|
||||
}
|
||||
root := repl.ReplaceAll(fsrv.Root, "")
|
||||
suffix := repl.ReplaceAll(r.URL.Path, "")
|
||||
filename := sanitizedPathJoin(root, suffix)
|
||||
|
||||
// get information about the file
|
||||
info, err := os.Stat(filename)
|
||||
|
|
@ -161,12 +113,8 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, _ cadd
|
|||
}
|
||||
|
||||
// we found an index file that might work,
|
||||
// so rewrite the request path and, if
|
||||
// configured, do an internal redirect
|
||||
// so rewrite the request path
|
||||
r.URL.Path = path.Join(r.URL.Path, indexPage)
|
||||
if fsrv.Rehandle {
|
||||
return caddyhttp.ErrRehandle
|
||||
}
|
||||
|
||||
info = indexInfo
|
||||
filename = indexPath
|
||||
|
|
@ -308,107 +256,12 @@ func sanitizedPathJoin(root, reqPath string) string {
|
|||
return filepath.Join(root, filepath.FromSlash(path.Clean("/"+reqPath)))
|
||||
}
|
||||
|
||||
// selectFile uses the specified selection policy (or first_existing
|
||||
// by default) to map the request r to a filename. The full path to
|
||||
// the file is returned if one is found; otherwise, an empty string
|
||||
// is returned.
|
||||
func (fsrv *FileServer) selectFile(r *http.Request, repl caddy.Replacer, filesToHide []string) string {
|
||||
root := repl.ReplaceAll(fsrv.Root, "")
|
||||
|
||||
if fsrv.Files == nil {
|
||||
return sanitizedPathJoin(root, r.URL.Path)
|
||||
}
|
||||
|
||||
switch fsrv.SelectionPolicy {
|
||||
case "", selectionPolicyFirstExisting:
|
||||
filesToHide := fsrv.transformHidePaths(repl)
|
||||
for _, f := range fsrv.Files {
|
||||
suffix := repl.ReplaceAll(f, "")
|
||||
fullpath := sanitizedPathJoin(root, suffix)
|
||||
if !fileHidden(fullpath, filesToHide) && fileExists(fullpath) {
|
||||
r.URL.Path = suffix
|
||||
return fullpath
|
||||
}
|
||||
}
|
||||
|
||||
case selectionPolicyLargestSize:
|
||||
var largestSize int64
|
||||
var largestFilename string
|
||||
var largestSuffix string
|
||||
for _, f := range fsrv.Files {
|
||||
suffix := repl.ReplaceAll(f, "")
|
||||
fullpath := sanitizedPathJoin(root, suffix)
|
||||
if fileHidden(fullpath, filesToHide) {
|
||||
continue
|
||||
}
|
||||
info, err := os.Stat(fullpath)
|
||||
if err == nil && info.Size() > largestSize {
|
||||
largestSize = info.Size()
|
||||
largestFilename = fullpath
|
||||
largestSuffix = suffix
|
||||
}
|
||||
}
|
||||
r.URL.Path = largestSuffix
|
||||
return largestFilename
|
||||
|
||||
case selectionPolicySmallestSize:
|
||||
var smallestSize int64
|
||||
var smallestFilename string
|
||||
var smallestSuffix string
|
||||
for _, f := range fsrv.Files {
|
||||
suffix := repl.ReplaceAll(f, "")
|
||||
fullpath := sanitizedPathJoin(root, suffix)
|
||||
if fileHidden(fullpath, filesToHide) {
|
||||
continue
|
||||
}
|
||||
info, err := os.Stat(fullpath)
|
||||
if err == nil && (smallestSize == 0 || info.Size() < smallestSize) {
|
||||
smallestSize = info.Size()
|
||||
smallestFilename = fullpath
|
||||
smallestSuffix = suffix
|
||||
}
|
||||
}
|
||||
r.URL.Path = smallestSuffix
|
||||
return smallestFilename
|
||||
|
||||
case selectionPolicyRecentlyMod:
|
||||
var recentDate time.Time
|
||||
var recentFilename string
|
||||
var recentSuffix string
|
||||
for _, f := range fsrv.Files {
|
||||
suffix := repl.ReplaceAll(f, "")
|
||||
fullpath := sanitizedPathJoin(root, suffix)
|
||||
if fileHidden(fullpath, filesToHide) {
|
||||
continue
|
||||
}
|
||||
info, err := os.Stat(fullpath)
|
||||
if err == nil &&
|
||||
(recentDate.IsZero() || info.ModTime().After(recentDate)) {
|
||||
recentDate = info.ModTime()
|
||||
recentFilename = fullpath
|
||||
recentSuffix = suffix
|
||||
}
|
||||
}
|
||||
r.URL.Path = recentSuffix
|
||||
return recentFilename
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// fileExists returns true if file exists.
|
||||
func fileExists(file string) bool {
|
||||
_, err := os.Stat(file)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
// fileHidden returns true if filename is hidden
|
||||
// according to the hide list.
|
||||
func fileHidden(filename string, hide []string) bool {
|
||||
nameOnly := filepath.Base(filename)
|
||||
sep := string(filepath.Separator)
|
||||
|
||||
// see if file is hidden
|
||||
for _, h := range hide {
|
||||
// assuming h is a glob/shell-like pattern,
|
||||
// use it to compare the whole file path;
|
||||
|
|
@ -453,6 +306,5 @@ const minBackoff, maxBackoff = 2, 5
|
|||
// Interface guards
|
||||
var (
|
||||
_ caddy.Provisioner = (*FileServer)(nil)
|
||||
_ caddy.Validator = (*FileServer)(nil)
|
||||
_ caddyhttp.MiddlewareHandler = (*FileServer)(nil)
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue