@marcel91200 / caddy Mirror · read-only / commits / a9698728
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 05:15 UTC

Default error handler; rename StaticFiles -> FileServer

Matthew Holt committed May 20, 2019 at 21:21 UTC a9698728506c580bc38db2e122a5e6ef07f85ce6
9 files changed +119 -77
cmd/caddy2/main.go
+1 -1
@@ -6,9 +6,9 @@ import (
6 // this is where modules get plugged in
7 _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp"
8 _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/caddylog"
9 + _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/fileserver"
10 _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/headers"
11 _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/reverseproxy"
11 - _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/staticfiles"
12 _ "bitbucket.org/lightcodelabs/caddy2/modules/caddytls"
13 )
14
modules/caddyhttp/fileserver/browse.go renamed
+14 -14
@@ -1,4 +1,4 @@
1 -package staticfiles
1 +package fileserver
2
3 import (
4 "bytes"
@@ -20,8 +20,8 @@ type Browse struct {
20 template *template.Template
21 }
22
23 -func (sf *StaticFiles) serveBrowse(dirPath string, w http.ResponseWriter, r *http.Request) error {
24 - dir, err := sf.openFile(dirPath, w)
23 +func (fsrv *FileServer) serveBrowse(dirPath string, w http.ResponseWriter, r *http.Request) error {
24 + dir, err := fsrv.openFile(dirPath, w)
25 if err != nil {
26 return err
27 }
@@ -29,7 +29,7 @@ func (sf *StaticFiles) serveBrowse(dirPath string, w http.ResponseWriter, r *htt
29
30 repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer)
31
32 - listing, err := sf.loadDirectoryContents(dir, r.URL.Path, repl)
32 + listing, err := fsrv.loadDirectoryContents(dir, r.URL.Path, repl)
33 switch {
34 case os.IsPermission(err):
35 return caddyhttp.Error(http.StatusForbidden, err)
@@ -39,18 +39,18 @@ func (sf *StaticFiles) serveBrowse(dirPath string, w http.ResponseWriter, r *htt
39 return caddyhttp.Error(http.StatusInternalServerError, err)
40 }
41
42 - sf.browseApplyQueryParams(w, r, &listing)
42 + fsrv.browseApplyQueryParams(w, r, &listing)
43
44 // write response as either JSON or HTML
45 var buf *bytes.Buffer
46 acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ","))
47 if strings.Contains(acceptHeader, "application/json") {
48 - if buf, err = sf.browseWriteJSON(listing); err != nil {
48 + if buf, err = fsrv.browseWriteJSON(listing); err != nil {
49 return caddyhttp.Error(http.StatusInternalServerError, err)
50 }
51 w.Header().Set("Content-Type", "application/json; charset=utf-8")
52 } else {
53 - if buf, err = sf.browseWriteHTML(listing); err != nil {
53 + if buf, err = fsrv.browseWriteHTML(listing); err != nil {
54 return caddyhttp.Error(http.StatusInternalServerError, err)
55 }
56 w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -75,7 +75,7 @@ func (sf *StaticFiles) serveBrowse(dirPath string, w http.ResponseWriter, r *htt
75 // return b.ServeListing(w, r, requestedFilepath, bc)
76 }
77
78 -func (sf *StaticFiles) loadDirectoryContents(dir *os.File, urlPath string, repl caddy2.Replacer) (browseListing, error) {
78 +func (fsrv *FileServer) loadDirectoryContents(dir *os.File, urlPath string, repl caddy2.Replacer) (browseListing, error) {
79 files, err := dir.Readdir(-1)
80 if err != nil {
81 return browseListing{}, err
@@ -83,14 +83,14 @@ func (sf *StaticFiles) loadDirectoryContents(dir *os.File, urlPath string, repl
83
84 // determine if user can browse up another folder
85 curPathDir := path.Dir(strings.TrimSuffix(urlPath, "/"))
86 - canGoUp := strings.HasPrefix(curPathDir, sf.Root)
86 + canGoUp := strings.HasPrefix(curPathDir, fsrv.Root)
87
88 - return sf.directoryListing(files, canGoUp, urlPath, repl), nil
88 + return fsrv.directoryListing(files, canGoUp, urlPath, repl), nil
89 }
90
91 // browseApplyQueryParams applies query parameters to the listing.
92 // It mutates the listing and may set cookies.
93 -func (sf *StaticFiles) browseApplyQueryParams(w http.ResponseWriter, r *http.Request, listing *browseListing) {
93 +func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Request, listing *browseListing) {
94 sortParam := r.URL.Query().Get("sort")
95 orderParam := r.URL.Query().Get("order")
96 limitParam := r.URL.Query().Get("limit")
@@ -121,15 +121,15 @@ func (sf *StaticFiles) browseApplyQueryParams(w http.ResponseWriter, r *http.Req
121 listing.applySortAndLimit(sortParam, orderParam, limitParam)
122 }
123
124 -func (sf *StaticFiles) browseWriteJSON(listing browseListing) (*bytes.Buffer, error) {
124 +func (fsrv *FileServer) browseWriteJSON(listing browseListing) (*bytes.Buffer, error) {
125 buf := new(bytes.Buffer)
126 err := json.NewEncoder(buf).Encode(listing.Items)
127 return buf, err
128 }
129
130 -func (sf *StaticFiles) browseWriteHTML(listing browseListing) (*bytes.Buffer, error) {
130 +func (fsrv *FileServer) browseWriteHTML(listing browseListing) (*bytes.Buffer, error) {
131 buf := new(bytes.Buffer)
132 - err := sf.Browse.template.Execute(buf, listing)
132 + err := fsrv.Browse.template.Execute(buf, listing)
133 return buf, err
134 }
135
modules/caddyhttp/fileserver/browselisting.go renamed
+3 -3
@@ -1,4 +1,4 @@
1 -package staticfiles
1 +package fileserver
2
3 import (
4 "net/url"
@@ -13,8 +13,8 @@ import (
13 "github.com/dustin/go-humanize"
14 )
15
16 -func (sf *StaticFiles) directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, repl caddy2.Replacer) browseListing {
17 - filesToHide := sf.transformHidePaths(repl)
16 +func (fsrv *FileServer) directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, repl caddy2.Replacer) browseListing {
17 + filesToHide := fsrv.transformHidePaths(repl)
18
19 var (
20 fileInfos []fileInfo
modules/caddyhttp/fileserver/browsetpl.go renamed
+1 -1
@@ -1,4 +1,4 @@
1 -package staticfiles
1 +package fileserver
2
3 const defaultBrowseTemplate = `<!DOCTYPE html>
4 <html>
modules/caddyhttp/fileserver/matcher.go renamed
+1 -1
@@ -1,4 +1,4 @@
1 -package staticfiles
1 +package fileserver
2
3 import (
4 "net/http"
modules/caddyhttp/fileserver/staticfiles.go renamed
+65 -47
@@ -1,4 +1,4 @@
1 -package staticfiles
1 +package fileserver
2
3 import (
4 "fmt"
@@ -20,13 +20,13 @@ func init() {
20 weakrand.Seed(time.Now().UnixNano())
21
22 caddy2.RegisterModule(caddy2.Module{
23 - Name: "http.responders.static_files",
24 - New: func() (interface{}, error) { return new(StaticFiles), nil },
23 + Name: "http.responders.file_server",
24 + New: func() (interface{}, error) { return new(FileServer), nil },
25 })
26 }
27
28 -// StaticFiles implements a static file server responder for Caddy.
29 -type StaticFiles struct {
28 +// FileServer implements a static file server responder for Caddy.
29 +type FileServer struct {
30 Root string `json:"root"` // default is current directory
31 Hide []string `json:"hide"`
32 IndexNames []string `json:"index_names"`
@@ -40,23 +40,23 @@ type StaticFiles struct {
40 }
41
42 // Provision sets up the static files responder.
43 -func (sf *StaticFiles) Provision(ctx caddy2.Context) error {
44 - if sf.Fallback != nil {
45 - err := sf.Fallback.Provision(ctx)
43 +func (fsrv *FileServer) Provision(ctx caddy2.Context) error {
44 + if fsrv.Fallback != nil {
45 + err := fsrv.Fallback.Provision(ctx)
46 if err != nil {
47 return fmt.Errorf("setting up fallback routes: %v", err)
48 }
49 }
50
51 - if sf.IndexNames == nil {
52 - sf.IndexNames = defaultIndexNames
51 + if fsrv.IndexNames == nil {
52 + fsrv.IndexNames = defaultIndexNames
53 }
54
55 - if sf.Browse != nil {
55 + if fsrv.Browse != nil {
56 var tpl *template.Template
57 var err error
58 - if sf.Browse.TemplateFile != "" {
59 - tpl, err = template.ParseFiles(sf.Browse.TemplateFile)
58 + if fsrv.Browse.TemplateFile != "" {
59 + tpl, err = template.ParseFiles(fsrv.Browse.TemplateFile)
60 if err != nil {
61 return fmt.Errorf("parsing browse template file: %v", err)
62 }
@@ -66,7 +66,7 @@ func (sf *StaticFiles) Provision(ctx caddy2.Context) error {
66 return fmt.Errorf("parsing default browse template: %v", err)
67 }
68 }
69 - sf.Browse.template = tpl
69 + fsrv.Browse.template = tpl
70 }
71
72 return nil
@@ -80,29 +80,31 @@ const (
80 )
81
82 // Validate ensures that sf has a valid configuration.
83 -func (sf *StaticFiles) Validate() error {
84 - switch sf.SelectionPolicy {
83 +func (fsrv *FileServer) Validate() error {
84 + switch fsrv.SelectionPolicy {
85 case "",
86 selectionPolicyFirstExisting,
87 selectionPolicyLargestSize,
88 selectionPolicySmallestSize,
89 selectionPolicyRecentlyMod:
90 default:
91 - return fmt.Errorf("unknown selection policy %s", sf.SelectionPolicy)
91 + return fmt.Errorf("unknown selection policy %s", fsrv.SelectionPolicy)
92 }
93 return nil
94 }
95
96 -func (sf *StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
96 +func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
97 repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer)
98
99 + filesToHide := fsrv.transformHidePaths(repl)
100 +
101 // map the request to a filename
102 pathBefore := r.URL.Path
101 - filename := sf.selectFile(r, repl)
103 + filename := fsrv.selectFile(r, repl, filesToHide)
104 if filename == "" {
105 // no files worked, so resort to fallback
104 - if sf.Fallback != nil {
105 - fallback := sf.Fallback.BuildCompositeRoute(w, r)
106 + if fsrv.Fallback != nil {
107 + fallback := fsrv.Fallback.BuildCompositeRoute(w, r)
108 return fallback.ServeHTTP(w, r)
109 }
110 return caddyhttp.Error(http.StatusNotFound, nil)
@@ -111,7 +113,7 @@ func (sf *StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
113 // if the ultimate destination has changed, submit
114 // this request for a rehandling (internal redirect)
115 // if configured to do so
114 - if r.URL.Path != pathBefore && sf.Rehandle {
116 + if r.URL.Path != pathBefore && fsrv.Rehandle {
117 return caddyhttp.ErrRehandle
118 }
119
@@ -130,10 +132,8 @@ func (sf *StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
132
133 // if the request mapped to a directory, see if
134 // there is an index file we can serve
133 - if info.IsDir() && len(sf.IndexNames) > 0 {
134 - filesToHide := sf.transformHidePaths(repl)
135 -
136 - for _, indexPage := range sf.IndexNames {
135 + if info.IsDir() && len(fsrv.IndexNames) > 0 {
136 + for _, indexPage := range fsrv.IndexNames {
137 indexPath := sanitizedPathJoin(filename, indexPage)
138 if fileHidden(indexPath, filesToHide) {
139 // pretend this file doesn't exist
@@ -149,7 +149,7 @@ func (sf *StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
149 // so rewrite the request path and, if
150 // configured, do an internal redirect
151 r.URL.Path = path.Join(r.URL.Path, indexPage)
152 - if sf.Rehandle {
152 + if fsrv.Rehandle {
153 return caddyhttp.ErrRehandle
154 }
155
@@ -162,16 +162,22 @@ func (sf *StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
162 // if still referencing a directory, delegate
163 // to browse or return an error
164 if info.IsDir() {
165 - if sf.Browse != nil {
166 - return sf.serveBrowse(filename, w, r)
165 + if fsrv.Browse != nil && !fileHidden(filename, filesToHide) {
166 + return fsrv.serveBrowse(filename, w, r)
167 }
168 return caddyhttp.Error(http.StatusNotFound, nil)
169 }
170
171 // TODO: content negotiation (brotli sidecar files, etc...)
172
173 + // one last check to ensure the file isn't hidden (we might
174 + // have changed the filename from when we last checked)
175 + if fileHidden(filename, filesToHide) {
176 + return caddyhttp.Error(http.StatusNotFound, nil)
177 + }
178 +
179 // open the file
174 - file, err := sf.openFile(filename, w)
180 + file, err := fsrv.openFile(filename, w)
181 if err != nil {
182 return err
183 }
@@ -179,6 +185,8 @@ func (sf *StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
185
186 // TODO: Etag
187
188 + // TODO: Disable content-type sniffing by setting a content-type...
189 +
190 // let the standard library do what it does best; note, however,
191 // that errors generated by ServeContent are written immediately
192 // to the response, so we cannot handle them (but errors here
@@ -192,7 +200,7 @@ func (sf *StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
200 // the response is configured to inform the client how to best handle it
201 // and a well-described handler error is returned (do not wrap the
202 // returned error value).
195 -func (sf *StaticFiles) openFile(filename string, w http.ResponseWriter) (*os.File, error) {
203 +func (fsrv *FileServer) openFile(filename string, w http.ResponseWriter) (*os.File, error) {
204 file, err := os.Open(filename)
205 if err != nil {
206 err = mapDirOpenError(err, filename)
@@ -239,11 +247,11 @@ func mapDirOpenError(originalErr error, name string) error {
247 }
248
249 // transformHidePaths performs replacements for all the elements of
242 -// sf.Hide and returns a new list of the transformed values.
243 -func (sf *StaticFiles) transformHidePaths(repl caddy2.Replacer) []string {
244 - hide := make([]string, len(sf.Hide))
245 - for i := range sf.Hide {
246 - hide[i] = repl.ReplaceAll(sf.Hide[i], "")
250 +// fsrv.Hide and returns a new list of the transformed values.
251 +func (fsrv *FileServer) transformHidePaths(repl caddy2.Replacer) []string {
252 + hide := make([]string, len(fsrv.Hide))
253 + for i := range fsrv.Hide {
254 + hide[i] = repl.ReplaceAll(fsrv.Hide[i], "")
255 }
256 return hide
257 }
@@ -251,7 +259,8 @@ func (sf *StaticFiles) transformHidePaths(repl caddy2.Replacer) []string {
259 // sanitizedPathJoin performs filepath.Join(root, reqPath) that
260 // is safe against directory traversal attacks. It uses logic
261 // similar to that in the Go standard library, specifically
254 -// in the implementation of http.Dir.
262 +// in the implementation of http.Dir. The root is assumed to
263 +// be a trusted path, but reqPath is not.
264 func sanitizedPathJoin(root, reqPath string) string {
265 // TODO: Caddy 1 uses this:
266 // prevent absolute path access on Windows, e.g. http://localhost:5000/C:\Windows\notepad.exe
@@ -276,17 +285,17 @@ func sanitizedPathJoin(root, reqPath string) string {
285 // by default) to map the request r to a filename. The full path to
286 // the file is returned if one is found; otherwise, an empty string
287 // is returned.
279 -func (sf *StaticFiles) selectFile(r *http.Request, repl caddy2.Replacer) string {
280 - root := repl.ReplaceAll(sf.Root, "")
288 +func (fsrv *FileServer) selectFile(r *http.Request, repl caddy2.Replacer, filesToHide []string) string {
289 + root := repl.ReplaceAll(fsrv.Root, "")
290
282 - if sf.Files == nil {
291 + if fsrv.Files == nil {
292 return sanitizedPathJoin(root, r.URL.Path)
293 }
294
286 - switch sf.SelectionPolicy {
295 + switch fsrv.SelectionPolicy {
296 case "", selectionPolicyFirstExisting:
288 - filesToHide := sf.transformHidePaths(repl)
289 - for _, f := range sf.Files {
297 + filesToHide := fsrv.transformHidePaths(repl)
298 + for _, f := range fsrv.Files {
299 suffix := repl.ReplaceAll(f, "")
300 fullpath := sanitizedPathJoin(root, suffix)
301 if !fileHidden(fullpath, filesToHide) && fileExists(fullpath) {
@@ -299,9 +308,12 @@ func (sf *StaticFiles) selectFile(r *http.Request, repl caddy2.Replacer) string
308 var largestSize int64
309 var largestFilename string
310 var largestSuffix string
302 - for _, f := range sf.Files {
311 + for _, f := range fsrv.Files {
312 suffix := repl.ReplaceAll(f, "")
313 fullpath := sanitizedPathJoin(root, suffix)
314 + if fileHidden(fullpath, filesToHide) {
315 + continue
316 + }
317 info, err := os.Stat(fullpath)
318 if err == nil && info.Size() > largestSize {
319 largestSize = info.Size()
@@ -316,9 +328,12 @@ func (sf *StaticFiles) selectFile(r *http.Request, repl caddy2.Replacer) string
328 var smallestSize int64
329 var smallestFilename string
330 var smallestSuffix string
319 - for _, f := range sf.Files {
331 + for _, f := range fsrv.Files {
332 suffix := repl.ReplaceAll(f, "")
333 fullpath := sanitizedPathJoin(root, suffix)
334 + if fileHidden(fullpath, filesToHide) {
335 + continue
336 + }
337 info, err := os.Stat(fullpath)
338 if err == nil && (smallestSize == 0 || info.Size() < smallestSize) {
339 smallestSize = info.Size()
@@ -333,9 +348,12 @@ func (sf *StaticFiles) selectFile(r *http.Request, repl caddy2.Replacer) string
348 var recentDate time.Time
349 var recentFilename string
350 var recentSuffix string
336 - for _, f := range sf.Files {
351 + for _, f := range fsrv.Files {
352 suffix := repl.ReplaceAll(f, "")
353 fullpath := sanitizedPathJoin(root, suffix)
354 + if fileHidden(fullpath, filesToHide) {
355 + continue
356 + }
357 info, err := os.Stat(fullpath)
358 if err == nil &&
359 (recentDate.IsZero() || info.ModTime().After(recentDate)) {
@@ -395,4 +413,4 @@ var defaultIndexNames = []string{"index.html"}
413 const minBackoff, maxBackoff = 2, 5
414
415 // Interface guard
398 -var _ caddyhttp.Handler = (*StaticFiles)(nil)
416 +var _ caddyhttp.Handler = (*FileServer)(nil)
modules/caddyhttp/fileserver/staticfiles_test.go renamed
+4 -1
@@ -1,4 +1,4 @@
1 -package staticfiles
1 +package fileserver
2
3 import (
4 "net/url"
@@ -60,6 +60,7 @@ func TestSanitizedPathJoin(t *testing.T) {
60 inputPath: "/%2e%2e%2f%2e%2e%2f",
61 expect: "/a/b",
62 },
63 + // TODO: test windows paths... on windows... sigh.
64 } {
65 // we don't *need* to use an actual parsed URL, but it
66 // adds some authenticity to the tests since real-world
@@ -76,3 +77,5 @@ func TestSanitizedPathJoin(t *testing.T) {
77 }
78 }
79 }
80 +
81 +// TODO: test fileHidden
modules/caddyhttp/server.go
+18 -5
@@ -5,6 +5,7 @@ import (
5 "fmt"
6 "log"
7 "net/http"
8 + "strconv"
9
10 "bitbucket.org/lightcodelabs/caddy2"
11 "bitbucket.org/lightcodelabs/caddy2/modules/caddytls"
@@ -41,15 +42,27 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
42 stack := s.Routes.BuildCompositeRoute(w, r)
43 err := s.executeCompositeRoute(w, r, stack)
44 if err != nil {
44 - // add the error value to the request context so
45 - // it can be accessed by error handlers
45 + // add the raw error value to the request context
46 + // so it can be accessed by error handlers
47 c := context.WithValue(r.Context(), ErrorCtxKey, err)
48 r = r.WithContext(c)
48 - // TODO: add error values to Replacer
49 +
50 + // add error values to the replacer
51 + repl.Set("http.error", err.Error())
52 + if handlerErr, ok := err.(HandlerError); ok {
53 + repl.Set("http.error.status_code", strconv.Itoa(handlerErr.StatusCode))
54 + repl.Set("http.error.status_text", http.StatusText(handlerErr.StatusCode))
55 + repl.Set("http.error.message", handlerErr.Message)
56 + repl.Set("http.error.trace", handlerErr.Trace)
57 + repl.Set("http.error.id", handlerErr.ID)
58 + }
59
60 if len(s.Errors.Routes) == 0 {
51 - // TODO: implement a default error handler?
52 - log.Printf("[ERROR] %s", err)
61 + // TODO: polish the default error handling
62 + log.Printf("[ERROR] Handler: %s", err)
63 + if handlerErr, ok := err.(HandlerError); ok {
64 + w.WriteHeader(handlerErr.StatusCode)
65 + }
66 } else {
67 errStack := s.Errors.Routes.BuildCompositeRoute(w, r)
68 err := s.executeCompositeRoute(w, r, errStack)
modules/caddyhttp/staticresp.go
+12 -4
@@ -3,6 +3,7 @@ package caddyhttp
3 import (
4 "fmt"
5 "net/http"
6 + "strconv"
7
8 "bitbucket.org/lightcodelabs/caddy2"
9 )
@@ -16,10 +17,11 @@ func init() {
17
18 // Static implements a simple responder for static responses.
19 type Static struct {
19 - StatusCode int `json:"status_code"`
20 - Headers http.Header `json:"headers"`
21 - Body string `json:"body"`
22 - Close bool `json:"close"`
20 + StatusCode int `json:"status_code"`
21 + StatusCodeStr string `json:"status_code_str"`
22 + Headers http.Header `json:"headers"`
23 + Body string `json:"body"`
24 + Close bool `json:"close"`
25 }
26
27 func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
@@ -39,6 +41,12 @@ func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
41
42 // write the headers with a status code
43 statusCode := s.StatusCode
44 + if statusCode == 0 && s.StatusCodeStr != "" {
45 + intVal, err := strconv.Atoi(repl.ReplaceAll(s.StatusCodeStr, ""))
46 + if err == nil {
47 + statusCode = intVal
48 + }
49 + }
50 if statusCode == 0 {
51 statusCode = http.StatusOK
52 }