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
}