From 4a3a418156e25aae17659142a4bf9259d7702c44 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 9 Jul 2019 12:58:39 -0600 Subject: Flatten HTTP handler config (#2662) Differentiating middleware and responders has one benefit, namely that it's clear which module provides the response, but even then it's not a great advantage. Linear handler config makes a little more sense, giving greater flexibility and simplifying the core a bit, even though it's slightly awkward that handlers which are responders may not use the 'next' handler that is passed in at all. --- modules/caddyhttp/caddyhttp.go | 60 ++++++------- modules/caddyhttp/encode/encode.go | 2 +- modules/caddyhttp/fileserver/staticfiles.go | 12 +-- modules/caddyhttp/rewrite/rewrite.go | 2 +- modules/caddyhttp/routes.go | 130 +++++++++------------------- modules/caddyhttp/server.go | 10 ++- modules/caddyhttp/staticresp.go | 24 +++-- modules/caddyhttp/staticresp_test.go | 5 +- modules/caddyhttp/templates/templates.go | 2 +- 9 files changed, 99 insertions(+), 148 deletions(-) diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index d0c7540..330e135 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -322,13 +322,15 @@ func (app *App) automaticHTTPS() error { MatchHost(domains), }, }, - responder: Static{ - StatusCode: http.StatusTemporaryRedirect, // TODO: use permanent redirect instead - Headers: http.Header{ - "Location": []string{redirTo}, - "Connection": []string{"close"}, + handlers: []MiddlewareHandler{ + Static{ + StatusCode: strconv.Itoa(http.StatusTemporaryRedirect), // TODO: use permanent redirect instead + Headers: http.Header{ + "Location": []string{redirTo}, + "Connection": []string{"close"}, + }, + Close: true, }, - Close: true, }, }) } @@ -381,34 +383,18 @@ func (app *App) listenerTaken(network, address string) bool { var defaultALPN = []string{"h2", "http/1.1"} // RequestMatcher is a type that can match to a request. -// A route matcher MUST NOT modify the request. +// A route matcher MUST NOT modify the request, with the +// only exception being its context. type RequestMatcher interface { Match(*http.Request) bool } -// Middleware chains one Handler to the next by being passed -// the next Handler in the chain. -type Middleware func(HandlerFunc) HandlerFunc - -// MiddlewareHandler is a Handler that includes a reference -// to the next middleware handler in the chain. Middleware -// handlers MUST NOT call Write() or WriteHeader() on the -// response writer; doing so will panic. See Handler godoc -// for more information. -type MiddlewareHandler interface { - ServeHTTP(http.ResponseWriter, *http.Request, Handler) error -} - // Handler is like http.Handler except ServeHTTP may return an error. // -// Middleware and responder handlers both implement this method. -// Middleware must not call Write or WriteHeader on the ResponseWriter; -// doing so will cause a panic. Responders should write to the response -// if there was not an error. -// // If any handler encounters an error, it should be returned for proper // handling. Return values should be propagated down the middleware chain -// by returning it unchanged. Returned errors should not be re-wrapped. +// by returning it unchanged. Returned errors should not be re-wrapped +// if they are already HandlerError values. type Handler interface { ServeHTTP(http.ResponseWriter, *http.Request) error } @@ -421,9 +407,25 @@ func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error { return f(w, r) } -// emptyHandler is used as a no-op handler, which is -// sometimes better than a nil Handler pointer. -var emptyHandler HandlerFunc = func(w http.ResponseWriter, r *http.Request) error { return nil } +// Middleware chains one Handler to the next by being passed +// the next Handler in the chain. +type Middleware func(HandlerFunc) HandlerFunc + +// MiddlewareHandler is like Handler except it takes as a third +// argument the next handler in the chain. The next handler will +// never be nil, but may be a no-op handler if this is the last +// handler in the chain. Handlers which act as middleware should +// call the next handler's ServeHTTP method so as to propagate +// the request down the chain properly. Handlers which act as +// responders (content origins) need not invoke the next handler, +// since the last handler in the chain should be the first to +// write the response. +type MiddlewareHandler interface { + ServeHTTP(http.ResponseWriter, *http.Request, Handler) error +} + +// emptyHandler is used as a no-op handler. +var emptyHandler HandlerFunc = func(http.ResponseWriter, *http.Request) error { return nil } const ( // DefaultHTTPPort is the default port for HTTP. diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index 2b45917..c78ccb9 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -36,7 +36,7 @@ import ( func init() { caddy.RegisterModule(caddy.Module{ - Name: "http.middleware.encode", + Name: "http.handlers.encode", New: func() interface{} { return new(Encode) }, }) } diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index 0bb910d..a7c72c9 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -35,7 +35,7 @@ func init() { weakrand.Seed(time.Now().UnixNano()) caddy.RegisterModule(caddy.Module{ - Name: "http.responders.file_server", + Name: "http.handlers.file_server", New: func() interface{} { return new(FileServer) }, }) } @@ -108,7 +108,7 @@ func (fsrv *FileServer) Validate() error { return nil } -func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) error { +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) @@ -119,7 +119,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) error if filename == "" { // no files worked, so resort to fallback if fsrv.Fallback != nil { - fallback, w := fsrv.Fallback.BuildCompositeRoute(w, r) + fallback := fsrv.Fallback.BuildCompositeRoute(w, r) return fallback.ServeHTTP(w, r) } return caddyhttp.Error(http.StatusNotFound, nil) @@ -452,7 +452,7 @@ const minBackoff, maxBackoff = 2, 5 // Interface guards var ( - _ caddy.Provisioner = (*FileServer)(nil) - _ caddy.Validator = (*FileServer)(nil) - _ caddyhttp.Handler = (*FileServer)(nil) + _ caddy.Provisioner = (*FileServer)(nil) + _ caddy.Validator = (*FileServer)(nil) + _ caddyhttp.MiddlewareHandler = (*FileServer)(nil) ) diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go index 9060e22..ac113ff 100644 --- a/modules/caddyhttp/rewrite/rewrite.go +++ b/modules/caddyhttp/rewrite/rewrite.go @@ -25,7 +25,7 @@ import ( func init() { caddy.RegisterModule(caddy.Module{ - Name: "http.middleware.rewrite", + Name: "http.handlers.rewrite", New: func() interface{} { return new(Rewrite) }, }) } diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go index 5e61fae..e010bb6 100644 --- a/modules/caddyhttp/routes.go +++ b/modules/caddyhttp/routes.go @@ -28,25 +28,19 @@ import ( type ServerRoute struct { Group string `json:"group,omitempty"` MatcherSets []map[string]json.RawMessage `json:"match,omitempty"` - Apply []json.RawMessage `json:"apply,omitempty"` - Respond json.RawMessage `json:"respond,omitempty"` - - Terminal bool `json:"terminal,omitempty"` + Handle []json.RawMessage `json:"handle,omitempty"` + Terminal bool `json:"terminal,omitempty"` // decoded values matcherSets []MatcherSet - middleware []MiddlewareHandler - responder Handler + handlers []MiddlewareHandler } // Empty returns true if the route has all zero/default values. func (sr ServerRoute) Empty() bool { return len(sr.MatcherSets) == 0 && - len(sr.Apply) == 0 && - len(sr.Respond) == 0 && - len(sr.matcherSets) == 0 && - len(sr.middleware) == 0 && - sr.responder == nil && + len(sr.Handle) == 0 && + len(sr.handlers) == 0 && !sr.Terminal && sr.Group == "" } @@ -98,40 +92,27 @@ func (routes RouteList) Provision(ctx caddy.Context) error { } routes[i].MatcherSets = nil // allow GC to deallocate - TODO: Does this help? - // middleware - for j, rawMsg := range route.Apply { - mid, err := ctx.LoadModuleInline("middleware", "http.middleware", rawMsg) - if err != nil { - return fmt.Errorf("loading middleware module in position %d: %v", j, err) - } - routes[i].middleware = append(routes[i].middleware, mid.(MiddlewareHandler)) - } - routes[i].Apply = nil // allow GC to deallocate - TODO: Does this help? - - // responder - if route.Respond != nil { - resp, err := ctx.LoadModuleInline("responder", "http.responders", route.Respond) + // handlers + for j, rawMsg := range route.Handle { + mh, err := ctx.LoadModuleInline("handler", "http.handlers", rawMsg) if err != nil { - return fmt.Errorf("loading responder module: %v", err) + return fmt.Errorf("loading handler module in position %d: %v", j, err) } - routes[i].responder = resp.(Handler) + routes[i].handlers = append(routes[i].handlers, mh.(MiddlewareHandler)) } - routes[i].Respond = nil // allow GC to deallocate - TODO: Does this help? + routes[i].Handle = nil // allow GC to deallocate - TODO: Does this help? } return nil } -// BuildCompositeRoute creates a chain of handlers by applying all the matching -// routes. The returned ResponseWriter should be used instead of rw. -func (routes RouteList) BuildCompositeRoute(rw http.ResponseWriter, req *http.Request) (Handler, http.ResponseWriter) { - mrw := &middlewareResponseWriter{ResponseWriterWrapper: &ResponseWriterWrapper{rw}} - +// BuildCompositeRoute creates a chain of handlers by +// applying all of the matching routes. +func (routes RouteList) BuildCompositeRoute(rw http.ResponseWriter, req *http.Request) Handler { if len(routes) == 0 { - return emptyHandler, mrw + return emptyHandler } var mid []Middleware - var responder Handler groups := make(map[string]struct{}) for _, route := range routes { @@ -140,9 +121,8 @@ func (routes RouteList) BuildCompositeRoute(rw http.ResponseWriter, req *http.Re continue } - // if route is part of a group, ensure only - // the first matching route in the group is - // applied + // if route is part of a group, ensure only the + // first matching route in the group is applied if route.Group != "" { _, ok := groups[route.Group] if ok { @@ -155,78 +135,48 @@ func (routes RouteList) BuildCompositeRoute(rw http.ResponseWriter, req *http.Re } // apply the rest of the route - for _, m := range route.middleware { - // we have to be sure to wrap m outside - // of our current scope so that the - // reference to this m isn't overwritten - // on the next iteration, leaving only - // the last middleware in the chain as - // the ONLY middleware in the chain! - mid = append(mid, wrapMiddleware(m)) - } - if responder == nil { - responder = route.responder + for _, mh := range route.handlers { + // we have to be sure to wrap mh outside + // of our current stack frame so that the + // reference to this mh isn't overwritten + // on the next iteration, leaving the last + // middleware in the chain as the ONLY + // middleware in the chain! + mid = append(mid, wrapMiddleware(mh)) } + + // if this route is supposed to be last, don't + // compile any more into the chain if route.Terminal { break } } - // build the middleware stack, with the responder at the end - stack := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { - if responder == nil { - return nil - } - mrw.allowWrites = true - return responder.ServeHTTP(w, r) - }) + // build the middleware chain, with the responder at the end + stack := emptyHandler for i := len(mid) - 1; i >= 0; i-- { stack = mid[i](stack) } - return stack, mrw + return stack } // wrapMiddleware wraps m such that it can be correctly -// appended to a list of middleware. This separate closure -// is necessary so that only the last middleware in a loop -// does not become the only middleware of the stack, -// repeatedly executed (i.e. it is necessary to keep a -// reference to this m outside of the scope of a loop)! -func wrapMiddleware(m MiddlewareHandler) Middleware { +// appended to a list of middleware. We can't do this +// directly in a loop because it relies on a reference +// to mh not changing until the execution of its handler, +// which is deferred by multiple func closures. In other +// words, we need to pull this particular MiddlewareHandler +// pointer into its own stack frame to preserve it so it +// won't be overwritten in future loop iterations. +func wrapMiddleware(mh MiddlewareHandler) Middleware { return func(next HandlerFunc) HandlerFunc { return func(w http.ResponseWriter, r *http.Request) error { // TODO: This is where request tracing could be implemented; also // see below to trace the responder as well // TODO: Trace a diff of the request, would be cool too! see what changed since the last middleware (host, headers, URI...) // TODO: see what the std lib gives us in terms of stack tracing too - return m.ServeHTTP(w, r, next) + return mh.ServeHTTP(w, r, next) } } } - -type middlewareResponseWriter struct { - *ResponseWriterWrapper - allowWrites bool -} - -func (mrw middlewareResponseWriter) WriteHeader(statusCode int) { - if !mrw.allowWrites { - // technically, this is not true: middleware can write headers, - // but only after the responder handler has returned; either the - // responder did nothing with the response (sad face), or the - // middleware wrapped the response and deferred the write - panic("WriteHeader: middleware cannot write response headers") - } - mrw.ResponseWriterWrapper.WriteHeader(statusCode) -} - -func (mrw middlewareResponseWriter) Write(b []byte) (int, error) { - if !mrw.allowWrites { - panic("Write: middleware cannot write to the response before responder") - } - return mrw.ResponseWriterWrapper.Write(b) -} - -// Interface guard -var _ HTTPInterfaces = (*middlewareResponseWriter)(nil) diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index d40a01d..8bc3a5a 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -65,9 +65,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { addHTTPVarsToReplacer(repl, r, w) // build and execute the main handler chain - stack, wrappedWriter := s.Routes.BuildCompositeRoute(w, r) + stack := s.Routes.BuildCompositeRoute(w, r) stack = s.wrapPrimaryRoute(stack) - err := s.executeCompositeRoute(wrappedWriter, r, stack) + err := s.executeCompositeRoute(w, r, stack) if err != nil { // add the raw error value to the request context // so it can be accessed by error handlers @@ -85,8 +85,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } if s.Errors != nil && len(s.Errors.Routes) > 0 { - errStack, wrappedWriter := s.Errors.Routes.BuildCompositeRoute(w, r) - err := s.executeCompositeRoute(wrappedWriter, r, errStack) + errStack := s.Errors.Routes.BuildCompositeRoute(w, r) + err := s.executeCompositeRoute(w, r, errStack) if err != nil { // TODO: what should we do if the error handler has an error? log.Printf("[ERROR] [%s %s] handling error: %v", r.Method, r.RequestURI, err) @@ -154,6 +154,8 @@ func (s *Server) enforcementHandler(w http.ResponseWriter, r *http.Request, next return next.ServeHTTP(w, r) } +// listenersUseAnyPortOtherThan returns true if there are any +// listeners in s that use a port which is not otherPort. func (s *Server) listenersUseAnyPortOtherThan(otherPort int) bool { for _, lnAddr := range s.Listen { _, addrs, err := caddy.ParseListenAddr(lnAddr) diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go index 3f43c92..8e4a3df 100644 --- a/modules/caddyhttp/staticresp.go +++ b/modules/caddyhttp/staticresp.go @@ -24,21 +24,20 @@ import ( func init() { caddy.RegisterModule(caddy.Module{ - Name: "http.responders.static", + Name: "http.handlers.static", New: func() interface{} { return new(Static) }, }) } // Static implements a simple responder for static responses. type Static struct { - StatusCode int `json:"status_code"` // TODO: should we turn this into a string so that only one field is needed? (string allows replacements) - StatusCodeStr string `json:"status_code_str"` - Headers http.Header `json:"headers"` - Body string `json:"body"` - Close bool `json:"close"` + StatusCode string `json:"status_code"` + Headers http.Header `json:"headers"` + Body string `json:"body"` + Close bool `json:"close"` } -func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error { +func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error { repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) // close the connection after responding @@ -60,16 +59,13 @@ func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error { } // get the status code - statusCode := s.StatusCode - if statusCode == 0 && s.StatusCodeStr != "" { - intVal, err := strconv.Atoi(repl.ReplaceAll(s.StatusCodeStr, "")) + statusCode := http.StatusOK + if s.StatusCode != "" { + intVal, err := strconv.Atoi(repl.ReplaceAll(s.StatusCode, "")) if err == nil { statusCode = intVal } } - if statusCode == 0 { - statusCode = http.StatusOK - } // write headers w.WriteHeader(statusCode) @@ -83,4 +79,4 @@ func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error { } // Interface guard -var _ Handler = (*Static)(nil) +var _ MiddlewareHandler = (*Static)(nil) diff --git a/modules/caddyhttp/staticresp_test.go b/modules/caddyhttp/staticresp_test.go index b518bb0..111e4f3 100644 --- a/modules/caddyhttp/staticresp_test.go +++ b/modules/caddyhttp/staticresp_test.go @@ -19,6 +19,7 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "strconv" "testing" "github.com/caddyserver/caddy/v2" @@ -29,7 +30,7 @@ func TestStaticResponseHandler(t *testing.T) { w := httptest.NewRecorder() s := Static{ - StatusCode: http.StatusNotFound, + StatusCode: strconv.Itoa(http.StatusNotFound), Headers: http.Header{ "X-Test": []string{"Testing"}, }, @@ -37,7 +38,7 @@ func TestStaticResponseHandler(t *testing.T) { Close: true, } - err := s.ServeHTTP(w, r) + err := s.ServeHTTP(w, r, nil) if err != nil { t.Errorf("did not expect an error, but got: %v", err) } diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go index e5bfa35..85b0bc2 100644 --- a/modules/caddyhttp/templates/templates.go +++ b/modules/caddyhttp/templates/templates.go @@ -28,7 +28,7 @@ import ( func init() { caddy.RegisterModule(caddy.Module{ - Name: "http.middleware.templates", + Name: "http.handlers.templates", New: func() interface{} { return new(Templates) }, }) } -- cgit v1.2.3 From eb8625f7744ba5e72b51549adc086e45313267cb Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 11 Jul 2019 17:02:57 -0600 Subject: Add error & subroute handlers; weakString; other minor handler changes --- modules/caddyhttp/caddyhttp.go | 4 +- modules/caddyhttp/fileserver/matcher.go | 177 ++++++++++++++++++++++------ modules/caddyhttp/fileserver/staticfiles.go | 164 ++------------------------ modules/caddyhttp/routes.go | 2 +- modules/caddyhttp/server.go | 4 +- modules/caddyhttp/staticerror.go | 95 +++++++++++++++ modules/caddyhttp/staticresp.go | 23 ++-- modules/caddyhttp/staticresp_test.go | 4 +- modules/caddyhttp/subroute.go | 60 ++++++++++ modules/caddyhttp/templates/templates.go | 10 +- 10 files changed, 331 insertions(+), 212 deletions(-) create mode 100644 modules/caddyhttp/staticerror.go create mode 100644 modules/caddyhttp/subroute.go diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index 330e135..ae73c98 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -323,8 +323,8 @@ func (app *App) automaticHTTPS() error { }, }, handlers: []MiddlewareHandler{ - Static{ - StatusCode: strconv.Itoa(http.StatusTemporaryRedirect), // TODO: use permanent redirect instead + StaticResponse{ + StatusCode: weakString(strconv.Itoa(http.StatusTemporaryRedirect)), // TODO: use permanent redirect instead Headers: http.Header{ "Location": []string{redirTo}, "Connection": []string{"close"}, diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go index da36eaa..023a69b 100644 --- a/modules/caddyhttp/fileserver/matcher.go +++ b/modules/caddyhttp/fileserver/matcher.go @@ -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"` +} + +// 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 } -// 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 +// 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 +} + +// fileExists returns true if file exists. +func fileExists(file string) bool { + _, err := os.Stat(file) + return !os.IsNotExist(err) } -// Interface guard -var _ caddyhttp.RequestMatcher = (*FileMatcher)(nil) +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) +) diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index a7c72c9..761dfc3 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -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) ) diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go index e010bb6..b0672b1 100644 --- a/modules/caddyhttp/routes.go +++ b/modules/caddyhttp/routes.go @@ -107,7 +107,7 @@ func (routes RouteList) Provision(ctx caddy.Context) error { // BuildCompositeRoute creates a chain of handlers by // 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 { return emptyHandler } diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 8bc3a5a..a24bbac 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -65,7 +65,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { addHTTPVarsToReplacer(repl, r, w) // build and execute the main handler chain - stack := s.Routes.BuildCompositeRoute(w, r) + stack := s.Routes.BuildCompositeRoute(r) stack = s.wrapPrimaryRoute(stack) err := s.executeCompositeRoute(w, r, stack) 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 { - errStack := s.Errors.Routes.BuildCompositeRoute(w, r) + errStack := s.Errors.Routes.BuildCompositeRoute(r) err := s.executeCompositeRoute(w, r, errStack) if err != nil { // TODO: what should we do if the error handler has an error? diff --git a/modules/caddyhttp/staticerror.go b/modules/caddyhttp/staticerror.go new file mode 100644 index 0000000..3a8e8bc --- /dev/null +++ b/modules/caddyhttp/staticerror.go @@ -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) +} diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go index 8e4a3df..291d992 100644 --- a/modules/caddyhttp/staticresp.go +++ b/modules/caddyhttp/staticresp.go @@ -24,20 +24,20 @@ import ( func init() { caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.static", - New: func() interface{} { return new(Static) }, + Name: "http.handlers.static_response", + New: func() interface{} { return new(StaticResponse) }, }) } -// Static implements a simple responder for static responses. -type Static struct { - StatusCode string `json:"status_code"` +// StaticResponse implements a simple responder for static responses. +type StaticResponse struct { + StatusCode weakString `json:"status_code"` Headers http.Header `json:"headers"` Body string `json:"body"` 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) // 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 statusCode := http.StatusOK - if s.StatusCode != "" { - intVal, err := strconv.Atoi(repl.ReplaceAll(s.StatusCode, "")) - if err == nil { - statusCode = intVal + if codeStr := s.StatusCode.String(); codeStr != "" { + intVal, err := strconv.Atoi(repl.ReplaceAll(codeStr, "")) + if err != nil { + return Error(http.StatusInternalServerError, err) } + statusCode = intVal } // write headers @@ -79,4 +80,4 @@ func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) err } // Interface guard -var _ MiddlewareHandler = (*Static)(nil) +var _ MiddlewareHandler = (*StaticResponse)(nil) diff --git a/modules/caddyhttp/staticresp_test.go b/modules/caddyhttp/staticresp_test.go index 111e4f3..49adedd 100644 --- a/modules/caddyhttp/staticresp_test.go +++ b/modules/caddyhttp/staticresp_test.go @@ -29,8 +29,8 @@ func TestStaticResponseHandler(t *testing.T) { r := fakeRequest() w := httptest.NewRecorder() - s := Static{ - StatusCode: strconv.Itoa(http.StatusNotFound), + s := StaticResponse{ + StatusCode: weakString(strconv.Itoa(http.StatusNotFound)), Headers: http.Header{ "X-Test": []string{"Testing"}, }, diff --git a/modules/caddyhttp/subroute.go b/modules/caddyhttp/subroute.go new file mode 100644 index 0000000..9172146 --- /dev/null +++ b/modules/caddyhttp/subroute.go @@ -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) +) diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go index 85b0bc2..9a41b6d 100644 --- a/modules/caddyhttp/templates/templates.go +++ b/modules/caddyhttp/templates/templates.go @@ -35,9 +35,9 @@ func init() { // Templates is a middleware which execute response bodies as templates. type Templates struct { - FileRoot string `json:"file_root,omitempty"` - MIMETypes []string `json:"mime_types,omitempty"` - Delimiters []string `json:"delimiters,omitempty"` + IncludeRoot string `json:"include_root,omitempty"` + MIMETypes []string `json:"mime_types,omitempty"` + Delimiters []string `json:"delimiters,omitempty"` } // 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. func (t *Templates) executeTemplate(rr caddyhttp.ResponseRecorder, r *http.Request) error { var fs http.FileSystem - if t.FileRoot != "" { - fs = http.Dir(t.FileRoot) + if t.IncludeRoot != "" { + fs = http.Dir(t.IncludeRoot) } ctx := &templateContext{ -- cgit v1.2.3