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
}