@marcel91200 / caddy Mirror · read-only / commits / 8d038ca5
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

fileserver: Improve and clarify file hiding logic (#3844)

* fileserver: Improve and clarify file hiding logic * Oops, forgot to run integration tests * Make this one integration test OS-agnostic * See if this appeases the Windows gods * D'oh

Matt Holt committed Nov 2, 2020 at 14:20 UTC 8d038ca515ffaa4d37dca05a95181203b2db64e7
5 files changed +119 -39
caddytest/integration/caddyfile_adapt/sort_directives_with_any_matcher_first.txt
+6 -10
@@ -1,9 +1,9 @@
1 :80
2
3 -file_server
3 +respond 200
4
5 @untrusted not remote_ip 10.1.1.0/24
6 -file_server @untrusted
6 +respond @untrusted 401
7 ----------
8 {
9 "apps": {
@@ -30,20 +30,16 @@ file_server @untrusted
30 ],
31 "handle": [
32 {
33 - "handler": "file_server",
34 - "hide": [
35 - "Caddyfile"
36 - ]
33 + "handler": "static_response",
34 + "status_code": 401
35 }
36 ]
37 },
38 {
39 "handle": [
40 {
43 - "handler": "file_server",
44 - "hide": [
45 - "Caddyfile"
46 - ]
41 + "handler": "static_response",
42 + "status_code": 200
43 }
44 ]
45 }
modules/caddyhttp/fileserver/caddyfile.go
+8
@@ -15,6 +15,7 @@
15 package fileserver
16
17 import (
18 + "path/filepath"
19 "strings"
20
21 "github.com/caddyserver/caddy/v2"
@@ -85,7 +86,14 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
86 // hide the Caddyfile (and any imported Caddyfiles)
87 if configFiles := h.Caddyfiles(); len(configFiles) > 0 {
88 for _, file := range configFiles {
89 + file = filepath.Clean(file)
90 if !fileHidden(file, fsrv.Hide) {
91 + // if there's no path separator, the file server module will hide all
92 + // files by that name, rather than a specific one; but we want to hide
93 + // only this specific file, so ensure there's always a path separator
94 + if !strings.Contains(file, separator) {
95 + file = "." + separator + file
96 + }
97 fsrv.Hide = append(fsrv.Hide, file)
98 }
99 }
modules/caddyhttp/fileserver/matcher.go
+1 -2
@@ -19,7 +19,6 @@ import (
19 "net/http"
20 "os"
21 "path"
22 - "path/filepath"
22 "strings"
23 "time"
24
@@ -286,7 +285,7 @@ func strictFileExists(file string) (os.FileInfo, bool) {
285 // https://stackoverflow.com/a/12518877/1048862
286 return nil, false
287 }
289 - if strings.HasSuffix(file, string(filepath.Separator)) {
288 + if strings.HasSuffix(file, separator) {
289 // by convention, file paths ending
290 // in a path separator must be a directory
291 return stat, stat.IsDir()
modules/caddyhttp/fileserver/staticfiles.go
+62 -19
@@ -46,7 +46,20 @@ type FileServer struct {
46 Root string `json:"root,omitempty"`
47
48 // A list of files or folders to hide; the file server will pretend as if
49 - // they don't exist. Accepts globular patterns like "*.hidden" or "/foo/*/bar".
49 + // they don't exist. Accepts globular patterns like "*.ext" or "/foo/*/bar"
50 + // as well as placeholders. Because site roots can be dynamic, this list
51 + // uses file system paths, not request paths. To clarify, the base of
52 + // relative paths is the current working directory, NOT the site root.
53 + //
54 + // Entries without a path separator (`/` or `\` depending on OS) will match
55 + // any file or directory of that name regardless of its path. To hide only a
56 + // specific file with a name that may not be unique, always use a path
57 + // separator. For example, to hide all files or folder trees named "hidden",
58 + // put "hidden" in the list. To hide only ./hidden, put "./hidden" in the list.
59 + //
60 + // When possible, all paths are resolved to their absolute form before
61 + // comparisons are made. For maximum clarity and explictness, use complete,
62 + // absolute paths; or, for greater portability, use relative paths instead.
63 Hide []string `json:"hide,omitempty"`
64
65 // The names of files to try as index files if a folder is requested.
@@ -101,6 +114,16 @@ func (fsrv *FileServer) Provision(ctx caddy.Context) error {
114 fsrv.Browse.template = tpl
115 }
116
117 + // for hide paths that are static (i.e. no placeholders), we can transform them into
118 + // absolute paths before the server starts for very slight performance improvement
119 + for i, h := range fsrv.Hide {
120 + if !strings.Contains(h, "{") && strings.Contains(h, separator) {
121 + if abs, err := filepath.Abs(h); err == nil {
122 + fsrv.Hide[i] = abs
123 + }
124 + }
125 + }
126 +
127 return nil
128 }
129
@@ -225,7 +248,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
248 }
249 }
250 w.WriteHeader(statusCode)
228 - if r.Method != "HEAD" {
251 + if r.Method != http.MethodHead {
252 io.Copy(w, file)
253 }
254 return nil
@@ -273,12 +296,12 @@ func mapDirOpenError(originalErr error, name string) error {
296 return originalErr
297 }
298
276 - parts := strings.Split(name, string(filepath.Separator))
299 + parts := strings.Split(name, separator)
300 for i := range parts {
301 if parts[i] == "" {
302 continue
303 }
281 - fi, err := os.Stat(strings.Join(parts[:i+1], string(filepath.Separator)))
304 + fi, err := os.Stat(strings.Join(parts[:i+1], separator))
305 if err != nil {
306 return originalErr
307 }
@@ -290,12 +313,19 @@ func mapDirOpenError(originalErr error, name string) error {
313 return originalErr
314 }
315
293 -// transformHidePaths performs replacements for all the elements of
294 -// fsrv.Hide and returns a new list of the transformed values.
316 +// transformHidePaths performs replacements for all the elements of fsrv.Hide and
317 +// makes them absolute paths (if they contain a path separator), then returns a
318 +// new list of the transformed values.
319 func (fsrv *FileServer) transformHidePaths(repl *caddy.Replacer) []string {
320 hide := make([]string, len(fsrv.Hide))
321 for i := range fsrv.Hide {
322 hide[i] = repl.ReplaceAll(fsrv.Hide[i], "")
323 + if strings.Contains(hide[i], separator) {
324 + abs, err := filepath.Abs(hide[i])
325 + if err == nil {
326 + hide[i] = abs
327 + }
328 + }
329 }
330 return hide
331 }
@@ -330,40 +360,50 @@ func sanitizedPathJoin(root, reqPath string) string {
360 // if the length is 1, then it's a path to the root,
361 // and that should return ".", so we don't append the separator.
362 if strings.HasSuffix(reqPath, "/") && len(reqPath) > 1 {
333 - path += string(filepath.Separator)
363 + path += separator
364 }
365
366 return path
367 }
368
339 -// fileHidden returns true if filename is hidden
340 -// according to the hide list.
369 +// fileHidden returns true if filename is hidden according to the hide list.
370 +// filename must be a relative or absolute file system path, not a request
371 +// URI path. It is expected that all the paths in the hide list are absolute
372 +// paths or are singular filenames (without a path separator).
373 func fileHidden(filename string, hide []string) bool {
342 - sep := string(filepath.Separator)
374 + if len(hide) == 0 {
375 + return false
376 + }
377 +
378 + // all path comparisons use the complete absolute path if possible
379 + filenameAbs, err := filepath.Abs(filename)
380 + if err == nil {
381 + filename = filenameAbs
382 + }
383 +
384 var components []string
385
386 for _, h := range hide {
346 - if !strings.Contains(h, sep) {
387 + if !strings.Contains(h, separator) {
388 // if there is no separator in h, then we assume the user
389 // wants to hide any files or folders that match that
390 // name; thus we have to compare against each component
391 // of the filename, e.g. hiding "bar" would hide "/bar"
392 // as well as "/foo/bar/baz" but not "/barstool".
393 if len(components) == 0 {
353 - components = strings.Split(filename, sep)
394 + components = strings.Split(filename, separator)
395 }
396 for _, c := range components {
356 - if c == h {
397 + if hidden, _ := filepath.Match(h, c); hidden {
398 return true
399 }
400 }
401 } else if strings.HasPrefix(filename, h) {
361 - // otherwise, if there is a separator in h, and
362 - // filename is exactly prefixed with h, then we
363 - // can do a prefix match so that "/foo" matches
364 - // "/foo/bar" but not "/foobar".
402 + // if there is a separator in h, and filename is exactly
403 + // prefixed with h, then we can do a prefix match so that
404 + // "/foo" matches "/foo/bar" but not "/foobar".
405 withoutPrefix := strings.TrimPrefix(filename, h)
366 - if strings.HasPrefix(withoutPrefix, sep) {
406 + if strings.HasPrefix(withoutPrefix, separator) {
407 return true
408 }
409 }
@@ -414,7 +454,10 @@ var bufPool = sync.Pool{
454 },
455 }
456
417 -const minBackoff, maxBackoff = 2, 5
457 +const (
458 + minBackoff, maxBackoff = 2, 5
459 + separator = string(filepath.Separator)
460 +)
461
462 // Interface guards
463 var (
modules/caddyhttp/fileserver/staticfiles_test.go
+42 -8
@@ -17,6 +17,8 @@ package fileserver
17 import (
18 "net/url"
19 "path/filepath"
20 + "runtime"
21 + "strings"
22 "testing"
23 )
24
@@ -44,7 +46,7 @@ func TestSanitizedPathJoin(t *testing.T) {
46 },
47 {
48 inputPath: "/foo/",
47 - expect: "foo" + string(filepath.Separator),
49 + expect: "foo" + separator,
50 },
51 {
52 inputPath: "/foo/bar",
@@ -77,7 +79,7 @@ func TestSanitizedPathJoin(t *testing.T) {
79 {
80 inputRoot: "/a/b",
81 inputPath: "/%2e%2e%2f%2e%2e%2f",
80 - expect: filepath.Join("/", "a", "b") + string(filepath.Separator),
82 + expect: filepath.Join("/", "a", "b") + separator,
83 },
84 {
85 inputRoot: "C:\\www",
@@ -154,6 +156,26 @@ func TestFileHidden(t *testing.T) {
156 inputPath: "/foo/asdf/bar",
157 expect: true,
158 },
159 + {
160 + inputHide: []string{"*.txt"},
161 + inputPath: "/foo/bar.txt",
162 + expect: true,
163 + },
164 + {
165 + inputHide: []string{"/foo/bar/*.txt"},
166 + inputPath: "/foo/bar/baz.txt",
167 + expect: true,
168 + },
169 + {
170 + inputHide: []string{"/foo/bar/*.txt"},
171 + inputPath: "/foo/bar.txt",
172 + expect: false,
173 + },
174 + {
175 + inputHide: []string{"/foo/bar/*.txt"},
176 + inputPath: "/foo/bar/index.html",
177 + expect: false,
178 + },
179 {
180 inputHide: []string{"/foo"},
181 inputPath: "/foo",
@@ -164,17 +186,29 @@ func TestFileHidden(t *testing.T) {
186 inputPath: "/foobar",
187 expect: false,
188 },
189 + {
190 + inputHide: []string{"first", "second"},
191 + inputPath: "/second",
192 + expect: true,
193 + },
194 } {
168 - // for Windows' sake
169 - tc.inputPath = filepath.FromSlash(tc.inputPath)
170 - for i := range tc.inputHide {
171 - tc.inputHide[i] = filepath.FromSlash(tc.inputHide[i])
195 + if runtime.GOOS == "windows" {
196 + if strings.HasPrefix(tc.inputPath, "/") {
197 + tc.inputPath, _ = filepath.Abs(tc.inputPath)
198 + }
199 + tc.inputPath = filepath.FromSlash(tc.inputPath)
200 + for i := range tc.inputHide {
201 + if strings.HasPrefix(tc.inputHide[i], "/") {
202 + tc.inputHide[i], _ = filepath.Abs(tc.inputHide[i])
203 + }
204 + tc.inputHide[i] = filepath.FromSlash(tc.inputHide[i])
205 + }
206 }
207
208 actual := fileHidden(tc.inputPath, tc.inputHide)
209 if actual != tc.expect {
176 - t.Errorf("Test %d: Is %s hidden in %v? Got %t but expected %t",
177 - i, tc.inputPath, tc.inputHide, actual, tc.expect)
210 + t.Errorf("Test %d: Does %v hide %s? Got %t but expected %t",
211 + i, tc.inputHide, tc.inputPath, actual, tc.expect)
212 }
213 }
214 }