mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-27 14:13:48 +03:00
Merge branch 'v2-handlers' into v2
# Conflicts: # modules/caddyhttp/caddyhttp.go # modules/caddyhttp/fileserver/staticfiles.go # modules/caddyhttp/routes.go # modules/caddyhttp/server.go # modules/caddyhttp/staticresp.go # modules/caddyhttp/staticresp_test.go
This commit is contained in:
commit
4698352b20
10 changed files with 331 additions and 212 deletions
|
@ -323,8 +323,8 @@ func (app *App) automaticHTTPS() error {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
handlers: []MiddlewareHandler{
|
handlers: []MiddlewareHandler{
|
||||||
Static{
|
StaticResponse{
|
||||||
StatusCode: strconv.Itoa(http.StatusTemporaryRedirect), // TODO: use permanent redirect instead
|
StatusCode: weakString(strconv.Itoa(http.StatusTemporaryRedirect)), // TODO: use permanent redirect instead
|
||||||
Headers: http.Header{
|
Headers: http.Header{
|
||||||
"Location": []string{redirTo},
|
"Location": []string{redirTo},
|
||||||
"Connection": []string{"close"},
|
"Connection": []string{"close"},
|
||||||
|
|
|
@ -15,55 +15,166 @@
|
||||||
package fileserver
|
package fileserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/modules/caddyhttp"
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(caddy.Module{
|
||||||
Name: "http.matchers.file",
|
Name: "http.matchers.file",
|
||||||
New: func() interface{} { return new(FileMatcher) },
|
New: func() interface{} { return new(MatchFile) },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileMatcher is a matcher that can match requests
|
// MatchFile is an HTTP request matcher that can match
|
||||||
// based on the local file system.
|
// requests based upon file existence.
|
||||||
// TODO: Not sure how to do this well; we'd need the ability to
|
type MatchFile struct {
|
||||||
// hide files, etc...
|
// The root directory, used for creating absolute
|
||||||
// TODO: Also consider a feature to match directory that
|
// file paths, and required when working with
|
||||||
// contains a certain filename (use filepath.Glob), useful
|
// relative paths; if not specified, the current
|
||||||
// if wanting to map directory-URI requests where the dir
|
// directory is assumed. Accepts placeholders.
|
||||||
// has index.php to PHP backends, for example (although this
|
Root string `json:"root,omitempty"`
|
||||||
// can effectively be done with rehandling already)
|
|
||||||
type FileMatcher struct {
|
// The list of files to try. Each path here is
|
||||||
Root string `json:"root"`
|
// considered relatice to Root. If nil, the
|
||||||
Path string `json:"path"`
|
// request URL's path will be assumed. Accepts
|
||||||
Flags []string `json:"flags"`
|
// 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.
|
// Validate ensures m has a valid configuration.
|
||||||
func (m FileMatcher) Match(r *http.Request) bool {
|
func (m MatchFile) Validate() error {
|
||||||
fullPath := sanitizedPathJoin(m.Root, m.Path)
|
switch m.TryPolicy {
|
||||||
var match bool
|
case "",
|
||||||
if len(m.Flags) > 0 {
|
tryPolicyFirstExist,
|
||||||
match = true
|
tryPolicyLargestSize,
|
||||||
fi, err := os.Stat(fullPath)
|
tryPolicySmallestSize,
|
||||||
for _, f := range m.Flags {
|
tryPolicyMostRecentMod:
|
||||||
switch f {
|
default:
|
||||||
case "EXIST":
|
return fmt.Errorf("unknown try policy %s", m.TryPolicy)
|
||||||
match = match && os.IsNotExist(err)
|
}
|
||||||
case "DIR":
|
return nil
|
||||||
match = match && err == nil && fi.IsDir()
|
}
|
||||||
default:
|
|
||||||
match = false
|
// 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
|
// fileExists returns true if file exists.
|
||||||
var _ caddyhttp.RequestMatcher = (*FileMatcher)(nil)
|
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.
|
// FileServer implements a static file server responder for Caddy.
|
||||||
type FileServer struct {
|
type FileServer struct {
|
||||||
Root string `json:"root,omitempty"` // default is current directory
|
Root string `json:"root,omitempty"` // default is current directory
|
||||||
Hide []string `json:"hide,omitempty"`
|
Hide []string `json:"hide,omitempty"`
|
||||||
IndexNames []string `json:"index_names,omitempty"`
|
IndexNames []string `json:"index_names,omitempty"`
|
||||||
Files []string `json:"files,omitempty"` // all relative to the root; default is request URI path
|
Browse *Browse `json:"browse,omitempty"`
|
||||||
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
|
|
||||||
// TODO: Content negotiation
|
// TODO: Content negotiation
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision sets up the static files responder.
|
// Provision sets up the static files responder.
|
||||||
func (fsrv *FileServer) Provision(ctx caddy.Context) error {
|
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 {
|
if fsrv.IndexNames == nil {
|
||||||
fsrv.IndexNames = defaultIndexNames
|
fsrv.IndexNames = defaultIndexNames
|
||||||
}
|
}
|
||||||
|
@ -87,50 +75,14 @@ func (fsrv *FileServer) Provision(ctx caddy.Context) error {
|
||||||
return nil
|
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 {
|
func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
|
||||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
|
||||||
|
|
||||||
filesToHide := fsrv.transformHidePaths(repl)
|
filesToHide := fsrv.transformHidePaths(repl)
|
||||||
|
|
||||||
// map the request to a filename
|
root := repl.ReplaceAll(fsrv.Root, "")
|
||||||
pathBefore := r.URL.Path
|
suffix := repl.ReplaceAll(r.URL.Path, "")
|
||||||
filename := fsrv.selectFile(r, repl, filesToHide)
|
filename := sanitizedPathJoin(root, suffix)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// get information about the file
|
// get information about the file
|
||||||
info, err := os.Stat(filename)
|
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,
|
// we found an index file that might work,
|
||||||
// so rewrite the request path and, if
|
// so rewrite the request path
|
||||||
// configured, do an internal redirect
|
|
||||||
r.URL.Path = path.Join(r.URL.Path, indexPage)
|
r.URL.Path = path.Join(r.URL.Path, indexPage)
|
||||||
if fsrv.Rehandle {
|
|
||||||
return caddyhttp.ErrRehandle
|
|
||||||
}
|
|
||||||
|
|
||||||
info = indexInfo
|
info = indexInfo
|
||||||
filename = indexPath
|
filename = indexPath
|
||||||
|
@ -308,107 +256,12 @@ func sanitizedPathJoin(root, reqPath string) string {
|
||||||
return filepath.Join(root, filepath.FromSlash(path.Clean("/"+reqPath)))
|
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
|
// fileHidden returns true if filename is hidden
|
||||||
// according to the hide list.
|
// according to the hide list.
|
||||||
func fileHidden(filename string, hide []string) bool {
|
func fileHidden(filename string, hide []string) bool {
|
||||||
nameOnly := filepath.Base(filename)
|
nameOnly := filepath.Base(filename)
|
||||||
sep := string(filepath.Separator)
|
sep := string(filepath.Separator)
|
||||||
|
|
||||||
// see if file is hidden
|
|
||||||
for _, h := range hide {
|
for _, h := range hide {
|
||||||
// assuming h is a glob/shell-like pattern,
|
// assuming h is a glob/shell-like pattern,
|
||||||
// use it to compare the whole file path;
|
// use it to compare the whole file path;
|
||||||
|
@ -453,6 +306,5 @@ const minBackoff, maxBackoff = 2, 5
|
||||||
// Interface guards
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ caddy.Provisioner = (*FileServer)(nil)
|
_ caddy.Provisioner = (*FileServer)(nil)
|
||||||
_ caddy.Validator = (*FileServer)(nil)
|
|
||||||
_ caddyhttp.MiddlewareHandler = (*FileServer)(nil)
|
_ caddyhttp.MiddlewareHandler = (*FileServer)(nil)
|
||||||
)
|
)
|
||||||
|
|
|
@ -107,7 +107,7 @@ func (routes RouteList) Provision(ctx caddy.Context) error {
|
||||||
|
|
||||||
// BuildCompositeRoute creates a chain of handlers by
|
// BuildCompositeRoute creates a chain of handlers by
|
||||||
// applying all of the matching routes.
|
// applying all of the matching routes.
|
||||||
func (routes RouteList) BuildCompositeRoute(rw http.ResponseWriter, req *http.Request) Handler {
|
func (routes RouteList) BuildCompositeRoute(req *http.Request) Handler {
|
||||||
if len(routes) == 0 {
|
if len(routes) == 0 {
|
||||||
return emptyHandler
|
return emptyHandler
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
addHTTPVarsToReplacer(repl, r, w)
|
addHTTPVarsToReplacer(repl, r, w)
|
||||||
|
|
||||||
// build and execute the main handler chain
|
// build and execute the main handler chain
|
||||||
stack := s.Routes.BuildCompositeRoute(w, r)
|
stack := s.Routes.BuildCompositeRoute(r)
|
||||||
stack = s.wrapPrimaryRoute(stack)
|
stack = s.wrapPrimaryRoute(stack)
|
||||||
err := s.executeCompositeRoute(w, r, stack)
|
err := s.executeCompositeRoute(w, r, stack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -85,7 +85,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.Errors != nil && len(s.Errors.Routes) > 0 {
|
if s.Errors != nil && len(s.Errors.Routes) > 0 {
|
||||||
errStack := s.Errors.Routes.BuildCompositeRoute(w, r)
|
errStack := s.Errors.Routes.BuildCompositeRoute(r)
|
||||||
err := s.executeCompositeRoute(w, r, errStack)
|
err := s.executeCompositeRoute(w, r, errStack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: what should we do if the error handler has an error?
|
// TODO: what should we do if the error handler has an error?
|
||||||
|
|
95
modules/caddyhttp/staticerror.go
Normal file
95
modules/caddyhttp/staticerror.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
package caddyhttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
caddy.RegisterModule(caddy.Module{
|
||||||
|
Name: "http.handlers.error",
|
||||||
|
New: func() interface{} { return new(StaticError) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// StaticError implements a simple handler that returns an error.
|
||||||
|
type StaticError struct {
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
StatusCode weakString `json:"status_code,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e StaticError) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error {
|
||||||
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
|
||||||
|
|
||||||
|
statusCode := http.StatusInternalServerError
|
||||||
|
if codeStr := e.StatusCode.String(); codeStr != "" {
|
||||||
|
intVal, err := strconv.Atoi(repl.ReplaceAll(codeStr, ""))
|
||||||
|
if err != nil {
|
||||||
|
return Error(http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
statusCode = intVal
|
||||||
|
}
|
||||||
|
|
||||||
|
return Error(statusCode, fmt.Errorf("%s", e.Error))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guard
|
||||||
|
var _ MiddlewareHandler = (*StaticError)(nil)
|
||||||
|
|
||||||
|
// weakString is a type that unmarshals any JSON value
|
||||||
|
// as a string literal, and provides methods for
|
||||||
|
// getting the value as different primitive types.
|
||||||
|
// However, using this type removes any type safety
|
||||||
|
// as far as deserializing JSON is concerned.
|
||||||
|
type weakString string
|
||||||
|
|
||||||
|
// UnmarshalJSON satisfies json.Unmarshaler. It
|
||||||
|
// unmarshals b by always interpreting it as a
|
||||||
|
// string literal.
|
||||||
|
func (ws *weakString) UnmarshalJSON(b []byte) error {
|
||||||
|
*ws = weakString(strings.Trim(string(b), `"`))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int returns ws as an integer. If ws is not an
|
||||||
|
// integer, 0 is returned.
|
||||||
|
func (ws weakString) Int() int {
|
||||||
|
num, _ := strconv.Atoi(string(ws))
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64 returns ws as a float64. If ws is not a
|
||||||
|
// float value, the zero value is returned.
|
||||||
|
func (ws weakString) Float64() float64 {
|
||||||
|
num, _ := strconv.ParseFloat(string(ws), 64)
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool returns ws as a boolean. If ws is not a
|
||||||
|
// boolean, false is returned.
|
||||||
|
func (ws weakString) Bool() bool {
|
||||||
|
return string(ws) == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns ws as a string.
|
||||||
|
func (ws weakString) String() string {
|
||||||
|
return string(ws)
|
||||||
|
}
|
|
@ -24,20 +24,20 @@ import (
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(caddy.Module{
|
||||||
Name: "http.handlers.static",
|
Name: "http.handlers.static_response",
|
||||||
New: func() interface{} { return new(Static) },
|
New: func() interface{} { return new(StaticResponse) },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static implements a simple responder for static responses.
|
// StaticResponse implements a simple responder for static responses.
|
||||||
type Static struct {
|
type StaticResponse struct {
|
||||||
StatusCode string `json:"status_code"`
|
StatusCode weakString `json:"status_code"`
|
||||||
Headers http.Header `json:"headers"`
|
Headers http.Header `json:"headers"`
|
||||||
Body string `json:"body"`
|
Body string `json:"body"`
|
||||||
Close bool `json:"close"`
|
Close bool `json:"close"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error {
|
func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error {
|
||||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
|
||||||
|
|
||||||
// close the connection after responding
|
// close the connection after responding
|
||||||
|
@ -60,11 +60,12 @@ func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) err
|
||||||
|
|
||||||
// get the status code
|
// get the status code
|
||||||
statusCode := http.StatusOK
|
statusCode := http.StatusOK
|
||||||
if s.StatusCode != "" {
|
if codeStr := s.StatusCode.String(); codeStr != "" {
|
||||||
intVal, err := strconv.Atoi(repl.ReplaceAll(s.StatusCode, ""))
|
intVal, err := strconv.Atoi(repl.ReplaceAll(codeStr, ""))
|
||||||
if err == nil {
|
if err != nil {
|
||||||
statusCode = intVal
|
return Error(http.StatusInternalServerError, err)
|
||||||
}
|
}
|
||||||
|
statusCode = intVal
|
||||||
}
|
}
|
||||||
|
|
||||||
// write headers
|
// write headers
|
||||||
|
@ -79,4 +80,4 @@ func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guard
|
// Interface guard
|
||||||
var _ MiddlewareHandler = (*Static)(nil)
|
var _ MiddlewareHandler = (*StaticResponse)(nil)
|
||||||
|
|
|
@ -29,8 +29,8 @@ func TestStaticResponseHandler(t *testing.T) {
|
||||||
r := fakeRequest()
|
r := fakeRequest()
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
s := Static{
|
s := StaticResponse{
|
||||||
StatusCode: strconv.Itoa(http.StatusNotFound),
|
StatusCode: weakString(strconv.Itoa(http.StatusNotFound)),
|
||||||
Headers: http.Header{
|
Headers: http.Header{
|
||||||
"X-Test": []string{"Testing"},
|
"X-Test": []string{"Testing"},
|
||||||
},
|
},
|
||||||
|
|
60
modules/caddyhttp/subroute.go
Normal file
60
modules/caddyhttp/subroute.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
package caddyhttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
caddy.RegisterModule(caddy.Module{
|
||||||
|
Name: "http.handlers.subroute",
|
||||||
|
New: func() interface{} { return new(Subroute) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subroute implements a handler that compiles and executes routes.
|
||||||
|
// This is useful for a batch of routes that all inherit the same
|
||||||
|
// matchers, or for routes with matchers that must be have deferred
|
||||||
|
// evaluation (e.g. if they depend on placeholders created by other
|
||||||
|
// matchers that need to be evaluated first).
|
||||||
|
type Subroute struct {
|
||||||
|
Routes RouteList `json:"routes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provision sets up subrouting.
|
||||||
|
func (sr *Subroute) Provision(ctx caddy.Context) error {
|
||||||
|
if sr.Routes != nil {
|
||||||
|
err := sr.Routes.Provision(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting up routes: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sr *Subroute) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error {
|
||||||
|
subroute := sr.Routes.BuildCompositeRoute(r)
|
||||||
|
return subroute.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards
|
||||||
|
var (
|
||||||
|
_ caddy.Provisioner = (*Subroute)(nil)
|
||||||
|
_ MiddlewareHandler = (*Subroute)(nil)
|
||||||
|
)
|
|
@ -35,9 +35,9 @@ func init() {
|
||||||
|
|
||||||
// Templates is a middleware which execute response bodies as templates.
|
// Templates is a middleware which execute response bodies as templates.
|
||||||
type Templates struct {
|
type Templates struct {
|
||||||
FileRoot string `json:"file_root,omitempty"`
|
IncludeRoot string `json:"include_root,omitempty"`
|
||||||
MIMETypes []string `json:"mime_types,omitempty"`
|
MIMETypes []string `json:"mime_types,omitempty"`
|
||||||
Delimiters []string `json:"delimiters,omitempty"`
|
Delimiters []string `json:"delimiters,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision provisions t.
|
// Provision provisions t.
|
||||||
|
@ -107,8 +107,8 @@ func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
|
||||||
// executeTemplate executes the template contained in wb.buf and replaces it with the results.
|
// executeTemplate executes the template contained in wb.buf and replaces it with the results.
|
||||||
func (t *Templates) executeTemplate(rr caddyhttp.ResponseRecorder, r *http.Request) error {
|
func (t *Templates) executeTemplate(rr caddyhttp.ResponseRecorder, r *http.Request) error {
|
||||||
var fs http.FileSystem
|
var fs http.FileSystem
|
||||||
if t.FileRoot != "" {
|
if t.IncludeRoot != "" {
|
||||||
fs = http.Dir(t.FileRoot)
|
fs = http.Dir(t.IncludeRoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := &templateContext{
|
ctx := &templateContext{
|
||||||
|
|
Loading…
Reference in a new issue