Source file
src/net/http/fs_test.go
1
2
3
4
5 package http_test
6
7 import (
8 "bufio"
9 "bytes"
10 "compress/gzip"
11 "errors"
12 "fmt"
13 "internal/testenv"
14 "io"
15 "io/fs"
16 "mime"
17 "mime/multipart"
18 "net"
19 "net/http"
20 . "net/http"
21 "net/http/httptest"
22 "net/url"
23 "os"
24 "os/exec"
25 "path"
26 "path/filepath"
27 "reflect"
28 "regexp"
29 "runtime"
30 "strings"
31 "testing"
32 "testing/fstest"
33 "time"
34 )
35
36 const (
37 testFile = "testdata/file"
38 testFileLen = 11
39 )
40
41 type wantRange struct {
42 start, end int64
43 }
44
45 var ServeFileRangeTests = []struct {
46 r string
47 code int
48 ranges []wantRange
49 }{
50 {r: "", code: StatusOK},
51 {r: "bytes=0-4", code: StatusPartialContent, ranges: []wantRange{{0, 5}}},
52 {r: "bytes=2-", code: StatusPartialContent, ranges: []wantRange{{2, testFileLen}}},
53 {r: "bytes=-5", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 5, testFileLen}}},
54 {r: "bytes=3-7", code: StatusPartialContent, ranges: []wantRange{{3, 8}}},
55 {r: "bytes=0-0,-2", code: StatusPartialContent, ranges: []wantRange{{0, 1}, {testFileLen - 2, testFileLen}}},
56 {r: "bytes=0-1,5-8", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, 9}}},
57 {r: "bytes=0-1,5-", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, testFileLen}}},
58 {r: "bytes=5-1000", code: StatusPartialContent, ranges: []wantRange{{5, testFileLen}}},
59 {r: "bytes=0-,1-,2-,3-,4-", code: StatusOK},
60 {r: "bytes=0-9", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen - 1}}},
61 {r: "bytes=0-10", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
62 {r: "bytes=0-11", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
63 {r: "bytes=10-11", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
64 {r: "bytes=10-", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
65 {r: "bytes=11-", code: StatusRequestedRangeNotSatisfiable},
66 {r: "bytes=11-12", code: StatusRequestedRangeNotSatisfiable},
67 {r: "bytes=12-12", code: StatusRequestedRangeNotSatisfiable},
68 {r: "bytes=11-100", code: StatusRequestedRangeNotSatisfiable},
69 {r: "bytes=12-100", code: StatusRequestedRangeNotSatisfiable},
70 {r: "bytes=100-", code: StatusRequestedRangeNotSatisfiable},
71 {r: "bytes=100-1000", code: StatusRequestedRangeNotSatisfiable},
72 }
73
74 func TestServeFile(t *testing.T) { run(t, testServeFile) }
75 func testServeFile(t *testing.T, mode testMode) {
76 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
77 ServeFile(w, r, "testdata/file")
78 })).ts
79 c := ts.Client()
80
81 var err error
82
83 file, err := os.ReadFile(testFile)
84 if err != nil {
85 t.Fatal("reading file:", err)
86 }
87
88
89 var req Request
90 req.Header = make(Header)
91 if req.URL, err = url.Parse(ts.URL); err != nil {
92 t.Fatal("ParseURL:", err)
93 }
94
95
96
97
98
99 for _, method := range []string{
100 MethodGet,
101 MethodPost,
102 MethodPut,
103 MethodPatch,
104 MethodDelete,
105 MethodOptions,
106 MethodTrace,
107 } {
108 req.Method = method
109 _, body := getBody(t, method, req, c)
110 if !bytes.Equal(body, file) {
111 t.Fatalf("body mismatch for %v request: got %q, want %q", method, body, file)
112 }
113 }
114
115
116 req.Method = MethodHead
117 resp, body := getBody(t, "HEAD", req, c)
118 if len(body) != 0 {
119 t.Fatalf("body mismatch for HEAD request: got %q, want empty", body)
120 }
121 if got, want := resp.Header.Get("Content-Length"), fmt.Sprint(len(file)); got != want {
122 t.Fatalf("Content-Length mismatch for HEAD request: got %v, want %v", got, want)
123 }
124
125
126 req.Method = MethodGet
127 Cases:
128 for _, rt := range ServeFileRangeTests {
129 if rt.r != "" {
130 req.Header.Set("Range", rt.r)
131 }
132 resp, body := getBody(t, fmt.Sprintf("range test %q", rt.r), req, c)
133 if resp.StatusCode != rt.code {
134 t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, resp.StatusCode, rt.code)
135 }
136 if rt.code == StatusRequestedRangeNotSatisfiable {
137 continue
138 }
139 wantContentRange := ""
140 if len(rt.ranges) == 1 {
141 rng := rt.ranges[0]
142 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
143 }
144 cr := resp.Header.Get("Content-Range")
145 if cr != wantContentRange {
146 t.Errorf("range=%q: Content-Range = %q, want %q", rt.r, cr, wantContentRange)
147 }
148 ct := resp.Header.Get("Content-Type")
149 if len(rt.ranges) == 1 {
150 rng := rt.ranges[0]
151 wantBody := file[rng.start:rng.end]
152 if !bytes.Equal(body, wantBody) {
153 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
154 }
155 if strings.HasPrefix(ct, "multipart/byteranges") {
156 t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct)
157 }
158 }
159 if len(rt.ranges) > 1 {
160 typ, params, err := mime.ParseMediaType(ct)
161 if err != nil {
162 t.Errorf("range=%q content-type = %q; %v", rt.r, ct, err)
163 continue
164 }
165 if typ != "multipart/byteranges" {
166 t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ)
167 continue
168 }
169 if params["boundary"] == "" {
170 t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct)
171 continue
172 }
173 if g, w := resp.ContentLength, int64(len(body)); g != w {
174 t.Errorf("range=%q Content-Length = %d; want %d", rt.r, g, w)
175 continue
176 }
177 mr := multipart.NewReader(bytes.NewReader(body), params["boundary"])
178 for ri, rng := range rt.ranges {
179 part, err := mr.NextPart()
180 if err != nil {
181 t.Errorf("range=%q, reading part index %d: %v", rt.r, ri, err)
182 continue Cases
183 }
184 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
185 if g, w := part.Header.Get("Content-Range"), wantContentRange; g != w {
186 t.Errorf("range=%q: part Content-Range = %q; want %q", rt.r, g, w)
187 }
188 body, err := io.ReadAll(part)
189 if err != nil {
190 t.Errorf("range=%q, reading part index %d body: %v", rt.r, ri, err)
191 continue Cases
192 }
193 wantBody := file[rng.start:rng.end]
194 if !bytes.Equal(body, wantBody) {
195 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
196 }
197 }
198 _, err = mr.NextPart()
199 if err != io.EOF {
200 t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err)
201 }
202 }
203 }
204 }
205
206 func TestServeFile_DotDot(t *testing.T) {
207 tests := []struct {
208 req string
209 wantStatus int
210 }{
211 {"/testdata/file", 200},
212 {"/../file", 400},
213 {"/..", 400},
214 {"/../", 400},
215 {"/../foo", 400},
216 {"/..\\foo", 400},
217 {"/file/a", 200},
218 {"/file/a..", 200},
219 {"/file/a/..", 400},
220 {"/file/a\\..", 400},
221 }
222 for _, tt := range tests {
223 req, err := ReadRequest(bufio.NewReader(strings.NewReader("GET " + tt.req + " HTTP/1.1\r\nHost: foo\r\n\r\n")))
224 if err != nil {
225 t.Errorf("bad request %q: %v", tt.req, err)
226 continue
227 }
228 rec := httptest.NewRecorder()
229 ServeFile(rec, req, "testdata/file")
230 if rec.Code != tt.wantStatus {
231 t.Errorf("for request %q, status = %d; want %d", tt.req, rec.Code, tt.wantStatus)
232 }
233 }
234 }
235
236
237 func TestServeFileDirPanicEmptyPath(t *testing.T) {
238 rec := httptest.NewRecorder()
239 req := httptest.NewRequest("GET", "/", nil)
240 req.URL.Path = ""
241 ServeFile(rec, req, "testdata")
242 res := rec.Result()
243 if res.StatusCode != 301 {
244 t.Errorf("code = %v; want 301", res.Status)
245 }
246 }
247
248
249 func TestServeContentWithEmptyContentIgnoreRanges(t *testing.T) {
250 for _, r := range []string{
251 "bytes=0-128",
252 "bytes=1-",
253 } {
254 rec := httptest.NewRecorder()
255 req := httptest.NewRequest("GET", "/", nil)
256 req.Header.Set("Range", r)
257 ServeContent(rec, req, "nothing", time.Now(), bytes.NewReader(nil))
258 res := rec.Result()
259 if res.StatusCode != 200 {
260 t.Errorf("code = %v; want 200", res.Status)
261 }
262 bodyLen := rec.Body.Len()
263 if bodyLen != 0 {
264 t.Errorf("body.Len() = %v; want 0", res.Status)
265 }
266 }
267 }
268
269 var fsRedirectTestData = []struct {
270 original, redirect string
271 }{
272 {"/test/index.html", "/test/"},
273 {"/test/testdata", "/test/testdata/"},
274 {"/test/testdata/file/", "/test/testdata/file"},
275 }
276
277 func TestFSRedirect(t *testing.T) { run(t, testFSRedirect) }
278 func testFSRedirect(t *testing.T, mode testMode) {
279 ts := newClientServerTest(t, mode, StripPrefix("/test", FileServer(Dir(".")))).ts
280
281 for _, data := range fsRedirectTestData {
282 res, err := ts.Client().Get(ts.URL + data.original)
283 if err != nil {
284 t.Fatal(err)
285 }
286 res.Body.Close()
287 if g, e := res.Request.URL.Path, data.redirect; g != e {
288 t.Errorf("redirect from %s: got %s, want %s", data.original, g, e)
289 }
290 }
291 }
292
293 type testFileSystem struct {
294 open func(name string) (File, error)
295 }
296
297 func (fs *testFileSystem) Open(name string) (File, error) {
298 return fs.open(name)
299 }
300
301 func TestFileServerCleans(t *testing.T) {
302 defer afterTest(t)
303 ch := make(chan string, 1)
304 fs := FileServer(&testFileSystem{func(name string) (File, error) {
305 ch <- name
306 return nil, errors.New("file does not exist")
307 }})
308 tests := []struct {
309 reqPath, openArg string
310 }{
311 {"/foo.txt", "/foo.txt"},
312 {"//foo.txt", "/foo.txt"},
313 {"/../foo.txt", "/foo.txt"},
314 }
315 req, _ := NewRequest("GET", "http://example.com", nil)
316 for n, test := range tests {
317 rec := httptest.NewRecorder()
318 req.URL.Path = test.reqPath
319 fs.ServeHTTP(rec, req)
320 if got := <-ch; got != test.openArg {
321 t.Errorf("test %d: got %q, want %q", n, got, test.openArg)
322 }
323 }
324 }
325
326 func TestFileServerEscapesNames(t *testing.T) { run(t, testFileServerEscapesNames) }
327 func testFileServerEscapesNames(t *testing.T, mode testMode) {
328 const dirListPrefix = "<pre>\n"
329 const dirListSuffix = "\n</pre>\n"
330 tests := []struct {
331 name, escaped string
332 }{
333 {`simple_name`, `<a href="simple_name">simple_name</a>`},
334 {`"'<>&`, `<a href="%22%27%3C%3E&">"'<>&</a>`},
335 {`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`},
336 {`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo"><combo>?foo</a>`},
337 {`foo:bar`, `<a href="./foo:bar">foo:bar</a>`},
338 }
339
340
341 fs := make(fakeFS)
342 for i, test := range tests {
343 testFile := &fakeFileInfo{basename: test.name}
344 fs[fmt.Sprintf("/%d", i)] = &fakeFileInfo{
345 dir: true,
346 modtime: time.Unix(1000000000, 0).UTC(),
347 ents: []*fakeFileInfo{testFile},
348 }
349 fs[fmt.Sprintf("/%d/%s", i, test.name)] = testFile
350 }
351
352 ts := newClientServerTest(t, mode, FileServer(&fs)).ts
353 for i, test := range tests {
354 url := fmt.Sprintf("%s/%d", ts.URL, i)
355 res, err := ts.Client().Get(url)
356 if err != nil {
357 t.Fatalf("test %q: Get: %v", test.name, err)
358 }
359 b, err := io.ReadAll(res.Body)
360 if err != nil {
361 t.Fatalf("test %q: read Body: %v", test.name, err)
362 }
363 s := string(b)
364 if !strings.HasPrefix(s, dirListPrefix) || !strings.HasSuffix(s, dirListSuffix) {
365 t.Errorf("test %q: listing dir, full output is %q, want prefix %q and suffix %q", test.name, s, dirListPrefix, dirListSuffix)
366 }
367 if trimmed := strings.TrimSuffix(strings.TrimPrefix(s, dirListPrefix), dirListSuffix); trimmed != test.escaped {
368 t.Errorf("test %q: listing dir, filename escaped to %q, want %q", test.name, trimmed, test.escaped)
369 }
370 res.Body.Close()
371 }
372 }
373
374 func TestFileServerSortsNames(t *testing.T) { run(t, testFileServerSortsNames) }
375 func testFileServerSortsNames(t *testing.T, mode testMode) {
376 const contents = "I am a fake file"
377 dirMod := time.Unix(123, 0).UTC()
378 fileMod := time.Unix(1000000000, 0).UTC()
379 fs := fakeFS{
380 "/": &fakeFileInfo{
381 dir: true,
382 modtime: dirMod,
383 ents: []*fakeFileInfo{
384 {
385 basename: "b",
386 modtime: fileMod,
387 contents: contents,
388 },
389 {
390 basename: "a",
391 modtime: fileMod,
392 contents: contents,
393 },
394 },
395 },
396 }
397
398 ts := newClientServerTest(t, mode, FileServer(&fs)).ts
399
400 res, err := ts.Client().Get(ts.URL)
401 if err != nil {
402 t.Fatalf("Get: %v", err)
403 }
404 defer res.Body.Close()
405
406 b, err := io.ReadAll(res.Body)
407 if err != nil {
408 t.Fatalf("read Body: %v", err)
409 }
410 s := string(b)
411 if !strings.Contains(s, "<a href=\"a\">a</a>\n<a href=\"b\">b</a>") {
412 t.Errorf("output appears to be unsorted:\n%s", s)
413 }
414 }
415
416 func mustRemoveAll(dir string) {
417 err := os.RemoveAll(dir)
418 if err != nil {
419 panic(err)
420 }
421 }
422
423 func TestFileServerImplicitLeadingSlash(t *testing.T) { run(t, testFileServerImplicitLeadingSlash) }
424 func testFileServerImplicitLeadingSlash(t *testing.T, mode testMode) {
425 tempDir := t.TempDir()
426 if err := os.WriteFile(filepath.Join(tempDir, "foo.txt"), []byte("Hello world"), 0644); err != nil {
427 t.Fatalf("WriteFile: %v", err)
428 }
429 ts := newClientServerTest(t, mode, StripPrefix("/bar/", FileServer(Dir(tempDir)))).ts
430 get := func(suffix string) string {
431 res, err := ts.Client().Get(ts.URL + suffix)
432 if err != nil {
433 t.Fatalf("Get %s: %v", suffix, err)
434 }
435 b, err := io.ReadAll(res.Body)
436 if err != nil {
437 t.Fatalf("ReadAll %s: %v", suffix, err)
438 }
439 res.Body.Close()
440 return string(b)
441 }
442 if s := get("/bar/"); !strings.Contains(s, ">foo.txt<") {
443 t.Logf("expected a directory listing with foo.txt, got %q", s)
444 }
445 if s := get("/bar/foo.txt"); s != "Hello world" {
446 t.Logf("expected %q, got %q", "Hello world", s)
447 }
448 }
449
450 func TestDirJoin(t *testing.T) {
451 if runtime.GOOS == "windows" {
452 t.Skip("skipping test on windows")
453 }
454 wfi, err := os.Stat("/etc/hosts")
455 if err != nil {
456 t.Skip("skipping test; no /etc/hosts file")
457 }
458 test := func(d Dir, name string) {
459 f, err := d.Open(name)
460 if err != nil {
461 t.Fatalf("open of %s: %v", name, err)
462 }
463 defer f.Close()
464 gfi, err := f.Stat()
465 if err != nil {
466 t.Fatalf("stat of %s: %v", name, err)
467 }
468 if !os.SameFile(gfi, wfi) {
469 t.Errorf("%s got different file", name)
470 }
471 }
472 test(Dir("/etc/"), "/hosts")
473 test(Dir("/etc/"), "hosts")
474 test(Dir("/etc/"), "../../../../hosts")
475 test(Dir("/etc"), "/hosts")
476 test(Dir("/etc"), "hosts")
477 test(Dir("/etc"), "../../../../hosts")
478
479
480
481 test(Dir("/etc/hosts"), "")
482 test(Dir("/etc/hosts"), "/")
483 test(Dir("/etc/hosts"), "../")
484 }
485
486 func TestEmptyDirOpenCWD(t *testing.T) {
487 test := func(d Dir) {
488 name := "fs_test.go"
489 f, err := d.Open(name)
490 if err != nil {
491 t.Fatalf("open of %s: %v", name, err)
492 }
493 defer f.Close()
494 }
495 test(Dir(""))
496 test(Dir("."))
497 test(Dir("./"))
498 }
499
500 func TestServeFileContentType(t *testing.T) { run(t, testServeFileContentType) }
501 func testServeFileContentType(t *testing.T, mode testMode) {
502 const ctype = "icecream/chocolate"
503 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
504 switch r.FormValue("override") {
505 case "1":
506 w.Header().Set("Content-Type", ctype)
507 case "2":
508
509 w.Header()["Content-Type"] = []string{}
510 }
511 ServeFile(w, r, "testdata/file")
512 })).ts
513 get := func(override string, want []string) {
514 resp, err := ts.Client().Get(ts.URL + "?override=" + override)
515 if err != nil {
516 t.Fatal(err)
517 }
518 if h := resp.Header["Content-Type"]; !reflect.DeepEqual(h, want) {
519 t.Errorf("Content-Type mismatch: got %v, want %v", h, want)
520 }
521 resp.Body.Close()
522 }
523 get("0", []string{"text/plain; charset=utf-8"})
524 get("1", []string{ctype})
525 get("2", nil)
526 }
527
528 func TestServeFileMimeType(t *testing.T) { run(t, testServeFileMimeType) }
529 func testServeFileMimeType(t *testing.T, mode testMode) {
530 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
531 ServeFile(w, r, "testdata/style.css")
532 })).ts
533 resp, err := ts.Client().Get(ts.URL)
534 if err != nil {
535 t.Fatal(err)
536 }
537 resp.Body.Close()
538 want := "text/css; charset=utf-8"
539 if h := resp.Header.Get("Content-Type"); h != want {
540 t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
541 }
542 }
543
544 func TestServeFileFromCWD(t *testing.T) { run(t, testServeFileFromCWD) }
545 func testServeFileFromCWD(t *testing.T, mode testMode) {
546 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
547 ServeFile(w, r, "fs_test.go")
548 })).ts
549 r, err := ts.Client().Get(ts.URL)
550 if err != nil {
551 t.Fatal(err)
552 }
553 r.Body.Close()
554 if r.StatusCode != 200 {
555 t.Fatalf("expected 200 OK, got %s", r.Status)
556 }
557 }
558
559
560 func TestServeDirWithoutTrailingSlash(t *testing.T) { run(t, testServeDirWithoutTrailingSlash) }
561 func testServeDirWithoutTrailingSlash(t *testing.T, mode testMode) {
562 e := "/testdata/"
563 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
564 ServeFile(w, r, ".")
565 })).ts
566 r, err := ts.Client().Get(ts.URL + "/testdata")
567 if err != nil {
568 t.Fatal(err)
569 }
570 r.Body.Close()
571 if g := r.Request.URL.Path; g != e {
572 t.Errorf("got %s, want %s", g, e)
573 }
574 }
575
576
577
578 func TestServeFileWithContentEncoding(t *testing.T) { run(t, testServeFileWithContentEncoding) }
579 func testServeFileWithContentEncoding(t *testing.T, mode testMode) {
580 cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
581 w.Header().Set("Content-Encoding", "foo")
582 ServeFile(w, r, "testdata/file")
583
584
585
586
587
588
589
590
591 w.(Flusher).Flush()
592 }))
593 resp, err := cst.c.Get(cst.ts.URL)
594 if err != nil {
595 t.Fatal(err)
596 }
597 resp.Body.Close()
598 if g, e := resp.ContentLength, int64(-1); g != e {
599 t.Errorf("Content-Length mismatch: got %d, want %d", g, e)
600 }
601 }
602
603
604
605 func TestServeFileNotModified(t *testing.T) { run(t, testServeFileNotModified) }
606 func testServeFileNotModified(t *testing.T, mode testMode) {
607 cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
608 w.Header().Set("Content-Type", "application/json")
609 w.Header().Set("Content-Encoding", "foo")
610 w.Header().Set("Etag", `"123"`)
611 ServeFile(w, r, "testdata/file")
612
613
614
615
616
617
618
619
620 w.(Flusher).Flush()
621 }))
622 req, err := NewRequest("GET", cst.ts.URL, nil)
623 if err != nil {
624 t.Fatal(err)
625 }
626 req.Header.Set("If-None-Match", `"123"`)
627 resp, err := cst.c.Do(req)
628 if err != nil {
629 t.Fatal(err)
630 }
631 b, err := io.ReadAll(resp.Body)
632 resp.Body.Close()
633 if err != nil {
634 t.Fatal("reading Body:", err)
635 }
636 if len(b) != 0 {
637 t.Errorf("non-empty body")
638 }
639 if g, e := resp.StatusCode, StatusNotModified; g != e {
640 t.Errorf("status mismatch: got %d, want %d", g, e)
641 }
642
643 if g, e1, e2 := resp.ContentLength, int64(-1), int64(0); g != e1 && g != e2 {
644 t.Errorf("Content-Length mismatch: got %d, want %d or %d", g, e1, e2)
645 }
646 if resp.Header.Get("Content-Type") != "" {
647 t.Errorf("Content-Type present, but it should not be")
648 }
649 if resp.Header.Get("Content-Encoding") != "" {
650 t.Errorf("Content-Encoding present, but it should not be")
651 }
652 }
653
654 func TestServeIndexHtml(t *testing.T) { run(t, testServeIndexHtml) }
655 func testServeIndexHtml(t *testing.T, mode testMode) {
656 for i := 0; i < 2; i++ {
657 var h Handler
658 var name string
659 switch i {
660 case 0:
661 h = FileServer(Dir("."))
662 name = "Dir"
663 case 1:
664 h = FileServer(FS(os.DirFS(".")))
665 name = "DirFS"
666 }
667 t.Run(name, func(t *testing.T) {
668 const want = "index.html says hello\n"
669 ts := newClientServerTest(t, mode, h).ts
670
671 for _, path := range []string{"/testdata/", "/testdata/index.html"} {
672 res, err := ts.Client().Get(ts.URL + path)
673 if err != nil {
674 t.Fatal(err)
675 }
676 b, err := io.ReadAll(res.Body)
677 if err != nil {
678 t.Fatal("reading Body:", err)
679 }
680 if s := string(b); s != want {
681 t.Errorf("for path %q got %q, want %q", path, s, want)
682 }
683 res.Body.Close()
684 }
685 })
686 }
687 }
688
689 func TestServeIndexHtmlFS(t *testing.T) { run(t, testServeIndexHtmlFS) }
690 func testServeIndexHtmlFS(t *testing.T, mode testMode) {
691 const want = "index.html says hello\n"
692 ts := newClientServerTest(t, mode, FileServer(Dir("."))).ts
693 defer ts.Close()
694
695 for _, path := range []string{"/testdata/", "/testdata/index.html"} {
696 res, err := ts.Client().Get(ts.URL + path)
697 if err != nil {
698 t.Fatal(err)
699 }
700 b, err := io.ReadAll(res.Body)
701 if err != nil {
702 t.Fatal("reading Body:", err)
703 }
704 if s := string(b); s != want {
705 t.Errorf("for path %q got %q, want %q", path, s, want)
706 }
707 res.Body.Close()
708 }
709 }
710
711 func TestFileServerZeroByte(t *testing.T) { run(t, testFileServerZeroByte) }
712 func testFileServerZeroByte(t *testing.T, mode testMode) {
713 ts := newClientServerTest(t, mode, FileServer(Dir("."))).ts
714
715 c, err := net.Dial("tcp", ts.Listener.Addr().String())
716 if err != nil {
717 t.Fatal(err)
718 }
719 defer c.Close()
720 _, err = fmt.Fprintf(c, "GET /..\x00 HTTP/1.0\r\n\r\n")
721 if err != nil {
722 t.Fatal(err)
723 }
724 var got bytes.Buffer
725 bufr := bufio.NewReader(io.TeeReader(c, &got))
726 res, err := ReadResponse(bufr, nil)
727 if err != nil {
728 t.Fatal("ReadResponse: ", err)
729 }
730 if res.StatusCode == 200 {
731 t.Errorf("got status 200; want an error. Body is:\n%s", got.Bytes())
732 }
733 }
734
735 func TestFileServerNamesEscape(t *testing.T) { run(t, testFileServerNamesEscape) }
736 func testFileServerNamesEscape(t *testing.T, mode testMode) {
737 ts := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts
738 for _, path := range []string{
739 "/../testdata/file",
740 "/NUL",
741 } {
742 res, err := ts.Client().Get(ts.URL + path)
743 if err != nil {
744 t.Fatal(err)
745 }
746 res.Body.Close()
747 if res.StatusCode < 400 || res.StatusCode > 599 {
748 t.Errorf("Get(%q): got status %v, want 4xx or 5xx", path, res.StatusCode)
749 }
750
751 }
752 }
753
754 type fakeFileInfo struct {
755 dir bool
756 basename string
757 modtime time.Time
758 ents []*fakeFileInfo
759 contents string
760 err error
761 }
762
763 func (f *fakeFileInfo) Name() string { return f.basename }
764 func (f *fakeFileInfo) Sys() any { return nil }
765 func (f *fakeFileInfo) ModTime() time.Time { return f.modtime }
766 func (f *fakeFileInfo) IsDir() bool { return f.dir }
767 func (f *fakeFileInfo) Size() int64 { return int64(len(f.contents)) }
768 func (f *fakeFileInfo) Mode() fs.FileMode {
769 if f.dir {
770 return 0755 | fs.ModeDir
771 }
772 return 0644
773 }
774
775 func (f *fakeFileInfo) String() string {
776 return fs.FormatFileInfo(f)
777 }
778
779 type fakeFile struct {
780 io.ReadSeeker
781 fi *fakeFileInfo
782 path string
783 entpos int
784 }
785
786 func (f *fakeFile) Close() error { return nil }
787 func (f *fakeFile) Stat() (fs.FileInfo, error) { return f.fi, nil }
788 func (f *fakeFile) Readdir(count int) ([]fs.FileInfo, error) {
789 if !f.fi.dir {
790 return nil, fs.ErrInvalid
791 }
792 var fis []fs.FileInfo
793
794 limit := f.entpos + count
795 if count <= 0 || limit > len(f.fi.ents) {
796 limit = len(f.fi.ents)
797 }
798 for ; f.entpos < limit; f.entpos++ {
799 fis = append(fis, f.fi.ents[f.entpos])
800 }
801
802 if len(fis) == 0 && count > 0 {
803 return fis, io.EOF
804 } else {
805 return fis, nil
806 }
807 }
808
809 type fakeFS map[string]*fakeFileInfo
810
811 func (fsys fakeFS) Open(name string) (File, error) {
812 name = path.Clean(name)
813 f, ok := fsys[name]
814 if !ok {
815 return nil, fs.ErrNotExist
816 }
817 if f.err != nil {
818 return nil, f.err
819 }
820 return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil
821 }
822
823 func TestDirectoryIfNotModified(t *testing.T) { run(t, testDirectoryIfNotModified) }
824 func testDirectoryIfNotModified(t *testing.T, mode testMode) {
825 const indexContents = "I am a fake index.html file"
826 fileMod := time.Unix(1000000000, 0).UTC()
827 fileModStr := fileMod.Format(TimeFormat)
828 dirMod := time.Unix(123, 0).UTC()
829 indexFile := &fakeFileInfo{
830 basename: "index.html",
831 modtime: fileMod,
832 contents: indexContents,
833 }
834 fs := fakeFS{
835 "/": &fakeFileInfo{
836 dir: true,
837 modtime: dirMod,
838 ents: []*fakeFileInfo{indexFile},
839 },
840 "/index.html": indexFile,
841 }
842
843 ts := newClientServerTest(t, mode, FileServer(fs)).ts
844
845 res, err := ts.Client().Get(ts.URL)
846 if err != nil {
847 t.Fatal(err)
848 }
849 b, err := io.ReadAll(res.Body)
850 if err != nil {
851 t.Fatal(err)
852 }
853 if string(b) != indexContents {
854 t.Fatalf("Got body %q; want %q", b, indexContents)
855 }
856 res.Body.Close()
857
858 lastMod := res.Header.Get("Last-Modified")
859 if lastMod != fileModStr {
860 t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr)
861 }
862
863 req, _ := NewRequest("GET", ts.URL, nil)
864 req.Header.Set("If-Modified-Since", lastMod)
865
866 c := ts.Client()
867 res, err = c.Do(req)
868 if err != nil {
869 t.Fatal(err)
870 }
871 if res.StatusCode != 304 {
872 t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode)
873 }
874 res.Body.Close()
875
876
877 indexFile.modtime = indexFile.modtime.Add(1 * time.Hour)
878
879 res, err = c.Do(req)
880 if err != nil {
881 t.Fatal(err)
882 }
883 if res.StatusCode != 200 {
884 t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res)
885 }
886 res.Body.Close()
887 }
888
889 func mustStat(t *testing.T, fileName string) fs.FileInfo {
890 fi, err := os.Stat(fileName)
891 if err != nil {
892 t.Fatal(err)
893 }
894 return fi
895 }
896
897 func TestServeContent(t *testing.T) { run(t, testServeContent) }
898 func testServeContent(t *testing.T, mode testMode) {
899 type serveParam struct {
900 name string
901 modtime time.Time
902 content io.ReadSeeker
903 contentType string
904 etag string
905 }
906 servec := make(chan serveParam, 1)
907 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
908 p := <-servec
909 if p.etag != "" {
910 w.Header().Set("ETag", p.etag)
911 }
912 if p.contentType != "" {
913 w.Header().Set("Content-Type", p.contentType)
914 }
915 ServeContent(w, r, p.name, p.modtime, p.content)
916 })).ts
917
918 type testCase struct {
919
920 file string
921 content io.ReadSeeker
922
923 modtime time.Time
924 serveETag string
925 serveContentType string
926 reqHeader map[string]string
927 wantLastMod string
928 wantContentType string
929 wantContentRange string
930 wantStatus int
931 }
932 htmlModTime := mustStat(t, "testdata/index.html").ModTime()
933 tests := map[string]testCase{
934 "no_last_modified": {
935 file: "testdata/style.css",
936 wantContentType: "text/css; charset=utf-8",
937 wantStatus: 200,
938 },
939 "with_last_modified": {
940 file: "testdata/index.html",
941 wantContentType: "text/html; charset=utf-8",
942 modtime: htmlModTime,
943 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
944 wantStatus: 200,
945 },
946 "not_modified_modtime": {
947 file: "testdata/style.css",
948 serveETag: `"foo"`,
949 modtime: htmlModTime,
950 reqHeader: map[string]string{
951 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
952 },
953 wantStatus: 304,
954 },
955 "not_modified_modtime_with_contenttype": {
956 file: "testdata/style.css",
957 serveContentType: "text/css",
958 serveETag: `"foo"`,
959 modtime: htmlModTime,
960 reqHeader: map[string]string{
961 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
962 },
963 wantStatus: 304,
964 },
965 "not_modified_etag": {
966 file: "testdata/style.css",
967 serveETag: `"foo"`,
968 reqHeader: map[string]string{
969 "If-None-Match": `"foo"`,
970 },
971 wantStatus: 304,
972 },
973 "not_modified_etag_no_seek": {
974 content: panicOnSeek{nil},
975 serveETag: `W/"foo"`,
976 reqHeader: map[string]string{
977 "If-None-Match": `"baz", W/"foo"`,
978 },
979 wantStatus: 304,
980 },
981 "if_none_match_mismatch": {
982 file: "testdata/style.css",
983 serveETag: `"foo"`,
984 reqHeader: map[string]string{
985 "If-None-Match": `"Foo"`,
986 },
987 wantStatus: 200,
988 wantContentType: "text/css; charset=utf-8",
989 },
990 "if_none_match_malformed": {
991 file: "testdata/style.css",
992 serveETag: `"foo"`,
993 reqHeader: map[string]string{
994 "If-None-Match": `,`,
995 },
996 wantStatus: 200,
997 wantContentType: "text/css; charset=utf-8",
998 },
999 "range_good": {
1000 file: "testdata/style.css",
1001 serveETag: `"A"`,
1002 reqHeader: map[string]string{
1003 "Range": "bytes=0-4",
1004 },
1005 wantStatus: StatusPartialContent,
1006 wantContentType: "text/css; charset=utf-8",
1007 wantContentRange: "bytes 0-4/8",
1008 },
1009 "range_match": {
1010 file: "testdata/style.css",
1011 serveETag: `"A"`,
1012 reqHeader: map[string]string{
1013 "Range": "bytes=0-4",
1014 "If-Range": `"A"`,
1015 },
1016 wantStatus: StatusPartialContent,
1017 wantContentType: "text/css; charset=utf-8",
1018 wantContentRange: "bytes 0-4/8",
1019 },
1020 "range_match_weak_etag": {
1021 file: "testdata/style.css",
1022 serveETag: `W/"A"`,
1023 reqHeader: map[string]string{
1024 "Range": "bytes=0-4",
1025 "If-Range": `W/"A"`,
1026 },
1027 wantStatus: 200,
1028 wantContentType: "text/css; charset=utf-8",
1029 },
1030 "range_no_overlap": {
1031 file: "testdata/style.css",
1032 serveETag: `"A"`,
1033 reqHeader: map[string]string{
1034 "Range": "bytes=10-20",
1035 },
1036 wantStatus: StatusRequestedRangeNotSatisfiable,
1037 wantContentType: "text/plain; charset=utf-8",
1038 wantContentRange: "bytes */8",
1039 },
1040
1041
1042 "range_no_match": {
1043 file: "testdata/style.css",
1044 serveETag: `"A"`,
1045 reqHeader: map[string]string{
1046 "Range": "bytes=0-4",
1047 "If-Range": `"B"`,
1048 },
1049 wantStatus: 200,
1050 wantContentType: "text/css; charset=utf-8",
1051 },
1052 "range_with_modtime": {
1053 file: "testdata/style.css",
1054 modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 , time.UTC),
1055 reqHeader: map[string]string{
1056 "Range": "bytes=0-4",
1057 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
1058 },
1059 wantStatus: StatusPartialContent,
1060 wantContentType: "text/css; charset=utf-8",
1061 wantContentRange: "bytes 0-4/8",
1062 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
1063 },
1064 "range_with_modtime_mismatch": {
1065 file: "testdata/style.css",
1066 modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 , time.UTC),
1067 reqHeader: map[string]string{
1068 "Range": "bytes=0-4",
1069 "If-Range": "Wed, 25 Jun 2014 17:12:19 GMT",
1070 },
1071 wantStatus: StatusOK,
1072 wantContentType: "text/css; charset=utf-8",
1073 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
1074 },
1075 "range_with_modtime_nanos": {
1076 file: "testdata/style.css",
1077 modtime: time.Date(2014, 6, 25, 17, 12, 18, 123 , time.UTC),
1078 reqHeader: map[string]string{
1079 "Range": "bytes=0-4",
1080 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
1081 },
1082 wantStatus: StatusPartialContent,
1083 wantContentType: "text/css; charset=utf-8",
1084 wantContentRange: "bytes 0-4/8",
1085 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
1086 },
1087 "unix_zero_modtime": {
1088 content: strings.NewReader("<html>foo"),
1089 modtime: time.Unix(0, 0),
1090 wantStatus: StatusOK,
1091 wantContentType: "text/html; charset=utf-8",
1092 },
1093 "ifmatch_matches": {
1094 file: "testdata/style.css",
1095 serveETag: `"A"`,
1096 reqHeader: map[string]string{
1097 "If-Match": `"Z", "A"`,
1098 },
1099 wantStatus: 200,
1100 wantContentType: "text/css; charset=utf-8",
1101 },
1102 "ifmatch_star": {
1103 file: "testdata/style.css",
1104 serveETag: `"A"`,
1105 reqHeader: map[string]string{
1106 "If-Match": `*`,
1107 },
1108 wantStatus: 200,
1109 wantContentType: "text/css; charset=utf-8",
1110 },
1111 "ifmatch_failed": {
1112 file: "testdata/style.css",
1113 serveETag: `"A"`,
1114 reqHeader: map[string]string{
1115 "If-Match": `"B"`,
1116 },
1117 wantStatus: 412,
1118 },
1119 "ifmatch_fails_on_weak_etag": {
1120 file: "testdata/style.css",
1121 serveETag: `W/"A"`,
1122 reqHeader: map[string]string{
1123 "If-Match": `W/"A"`,
1124 },
1125 wantStatus: 412,
1126 },
1127 "if_unmodified_since_true": {
1128 file: "testdata/style.css",
1129 modtime: htmlModTime,
1130 reqHeader: map[string]string{
1131 "If-Unmodified-Since": htmlModTime.UTC().Format(TimeFormat),
1132 },
1133 wantStatus: 200,
1134 wantContentType: "text/css; charset=utf-8",
1135 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
1136 },
1137 "if_unmodified_since_false": {
1138 file: "testdata/style.css",
1139 modtime: htmlModTime,
1140 reqHeader: map[string]string{
1141 "If-Unmodified-Since": htmlModTime.Add(-2 * time.Second).UTC().Format(TimeFormat),
1142 },
1143 wantStatus: 412,
1144 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
1145 },
1146 }
1147 for testName, tt := range tests {
1148 var content io.ReadSeeker
1149 if tt.file != "" {
1150 f, err := os.Open(tt.file)
1151 if err != nil {
1152 t.Fatalf("test %q: %v", testName, err)
1153 }
1154 defer f.Close()
1155 content = f
1156 } else {
1157 content = tt.content
1158 }
1159 for _, method := range []string{"GET", "HEAD"} {
1160
1161 if content, ok := content.(*strings.Reader); ok {
1162 content.Seek(0, io.SeekStart)
1163 }
1164
1165 servec <- serveParam{
1166 name: filepath.Base(tt.file),
1167 content: content,
1168 modtime: tt.modtime,
1169 etag: tt.serveETag,
1170 contentType: tt.serveContentType,
1171 }
1172 req, err := NewRequest(method, ts.URL, nil)
1173 if err != nil {
1174 t.Fatal(err)
1175 }
1176 for k, v := range tt.reqHeader {
1177 req.Header.Set(k, v)
1178 }
1179
1180 c := ts.Client()
1181 res, err := c.Do(req)
1182 if err != nil {
1183 t.Fatal(err)
1184 }
1185 io.Copy(io.Discard, res.Body)
1186 res.Body.Close()
1187 if res.StatusCode != tt.wantStatus {
1188 t.Errorf("test %q using %q: got status = %d; want %d", testName, method, res.StatusCode, tt.wantStatus)
1189 }
1190 if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e {
1191 t.Errorf("test %q using %q: got content-type = %q, want %q", testName, method, g, e)
1192 }
1193 if g, e := res.Header.Get("Content-Range"), tt.wantContentRange; g != e {
1194 t.Errorf("test %q using %q: got content-range = %q, want %q", testName, method, g, e)
1195 }
1196 if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e {
1197 t.Errorf("test %q using %q: got last-modified = %q, want %q", testName, method, g, e)
1198 }
1199 }
1200 }
1201 }
1202
1203
1204 func TestServerFileStatError(t *testing.T) {
1205 rec := httptest.NewRecorder()
1206 r, _ := NewRequest("GET", "http://foo/", nil)
1207 redirect := false
1208 name := "file.txt"
1209 fs := issue12991FS{}
1210 ExportServeFile(rec, r, fs, name, redirect)
1211 if body := rec.Body.String(); !strings.Contains(body, "403") || !strings.Contains(body, "Forbidden") {
1212 t.Errorf("wanted 403 forbidden message; got: %s", body)
1213 }
1214 }
1215
1216 type issue12991FS struct{}
1217
1218 func (issue12991FS) Open(string) (File, error) { return issue12991File{}, nil }
1219
1220 type issue12991File struct{ File }
1221
1222 func (issue12991File) Stat() (fs.FileInfo, error) { return nil, fs.ErrPermission }
1223 func (issue12991File) Close() error { return nil }
1224
1225 func TestServeContentErrorMessages(t *testing.T) { run(t, testServeContentErrorMessages) }
1226 func testServeContentErrorMessages(t *testing.T, mode testMode) {
1227 fs := fakeFS{
1228 "/500": &fakeFileInfo{
1229 err: errors.New("random error"),
1230 },
1231 "/403": &fakeFileInfo{
1232 err: &fs.PathError{Err: fs.ErrPermission},
1233 },
1234 }
1235 ts := newClientServerTest(t, mode, FileServer(fs)).ts
1236 c := ts.Client()
1237 for _, code := range []int{403, 404, 500} {
1238 res, err := c.Get(fmt.Sprintf("%s/%d", ts.URL, code))
1239 if err != nil {
1240 t.Errorf("Error fetching /%d: %v", code, err)
1241 continue
1242 }
1243 if res.StatusCode != code {
1244 t.Errorf("For /%d, status code = %d; want %d", code, res.StatusCode, code)
1245 }
1246 res.Body.Close()
1247 }
1248 }
1249
1250
1251 func TestLinuxSendfile(t *testing.T) {
1252 setParallel(t)
1253 defer afterTest(t)
1254 if runtime.GOOS != "linux" {
1255 t.Skip("skipping; linux-only test")
1256 }
1257 if _, err := exec.LookPath("strace"); err != nil {
1258 t.Skip("skipping; strace not found in path")
1259 }
1260
1261 ln, err := net.Listen("tcp", "127.0.0.1:0")
1262 if err != nil {
1263 t.Fatal(err)
1264 }
1265 lnf, err := ln.(*net.TCPListener).File()
1266 if err != nil {
1267 t.Fatal(err)
1268 }
1269 defer ln.Close()
1270
1271
1272 if err := testenv.Command(t, "strace", "-f", "-q", os.Args[0], "-test.run=^$").Run(); err != nil {
1273 t.Skipf("skipping; failed to run strace: %v", err)
1274 }
1275
1276 filename := fmt.Sprintf("1kb-%d", os.Getpid())
1277 filepath := path.Join(os.TempDir(), filename)
1278
1279 if err := os.WriteFile(filepath, bytes.Repeat([]byte{'a'}, 1<<10), 0755); err != nil {
1280 t.Fatal(err)
1281 }
1282 defer os.Remove(filepath)
1283
1284 var buf strings.Builder
1285 child := testenv.Command(t, "strace", "-f", "-q", os.Args[0], "-test.run=^TestLinuxSendfileChild$")
1286 child.ExtraFiles = append(child.ExtraFiles, lnf)
1287 child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
1288 child.Stdout = &buf
1289 child.Stderr = &buf
1290 if err := child.Start(); err != nil {
1291 t.Skipf("skipping; failed to start straced child: %v", err)
1292 }
1293
1294 res, err := Get(fmt.Sprintf("http://%s/%s", ln.Addr(), filename))
1295 if err != nil {
1296 t.Fatalf("http client error: %v", err)
1297 }
1298 _, err = io.Copy(io.Discard, res.Body)
1299 if err != nil {
1300 t.Fatalf("client body read error: %v", err)
1301 }
1302 res.Body.Close()
1303
1304
1305 Post(fmt.Sprintf("http://%s/quit", ln.Addr()), "", nil)
1306 child.Wait()
1307
1308 rx := regexp.MustCompile(`\b(n64:)?sendfile(64)?\(`)
1309 out := buf.String()
1310 if !rx.MatchString(out) {
1311 t.Errorf("no sendfile system call found in:\n%s", out)
1312 }
1313 }
1314
1315 func getBody(t *testing.T, testName string, req Request, client *Client) (*Response, []byte) {
1316 r, err := client.Do(&req)
1317 if err != nil {
1318 t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err)
1319 }
1320 b, err := io.ReadAll(r.Body)
1321 if err != nil {
1322 t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err)
1323 }
1324 return r, b
1325 }
1326
1327
1328
1329 func TestLinuxSendfileChild(*testing.T) {
1330 if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
1331 return
1332 }
1333 defer os.Exit(0)
1334 fd3 := os.NewFile(3, "ephemeral-port-listener")
1335 ln, err := net.FileListener(fd3)
1336 if err != nil {
1337 panic(err)
1338 }
1339 mux := NewServeMux()
1340 mux.Handle("/", FileServer(Dir(os.TempDir())))
1341 mux.HandleFunc("/quit", func(ResponseWriter, *Request) {
1342 os.Exit(0)
1343 })
1344 s := &Server{Handler: mux}
1345 err = s.Serve(ln)
1346 if err != nil {
1347 panic(err)
1348 }
1349 }
1350
1351
1352 func TestFileServerNotDirError(t *testing.T) {
1353 run(t, func(t *testing.T, mode testMode) {
1354 t.Run("Dir", func(t *testing.T) {
1355 testFileServerNotDirError(t, mode, func(path string) FileSystem { return Dir(path) })
1356 })
1357 t.Run("FS", func(t *testing.T) {
1358 testFileServerNotDirError(t, mode, func(path string) FileSystem { return FS(os.DirFS(path)) })
1359 })
1360 })
1361 }
1362
1363 func testFileServerNotDirError(t *testing.T, mode testMode, newfs func(string) FileSystem) {
1364 ts := newClientServerTest(t, mode, FileServer(newfs("testdata"))).ts
1365
1366 res, err := ts.Client().Get(ts.URL + "/index.html/not-a-file")
1367 if err != nil {
1368 t.Fatal(err)
1369 }
1370 res.Body.Close()
1371 if res.StatusCode != 404 {
1372 t.Errorf("StatusCode = %v; want 404", res.StatusCode)
1373 }
1374
1375 test := func(name string, fsys FileSystem) {
1376 t.Run(name, func(t *testing.T) {
1377 _, err = fsys.Open("/index.html/not-a-file")
1378 if err == nil {
1379 t.Fatal("err == nil; want != nil")
1380 }
1381 if !errors.Is(err, fs.ErrNotExist) {
1382 t.Errorf("err = %v; errors.Is(err, fs.ErrNotExist) = %v; want true", err,
1383 errors.Is(err, fs.ErrNotExist))
1384 }
1385
1386 _, err = fsys.Open("/index.html/not-a-dir/not-a-file")
1387 if err == nil {
1388 t.Fatal("err == nil; want != nil")
1389 }
1390 if !errors.Is(err, fs.ErrNotExist) {
1391 t.Errorf("err = %v; errors.Is(err, fs.ErrNotExist) = %v; want true", err,
1392 errors.Is(err, fs.ErrNotExist))
1393 }
1394 })
1395 }
1396
1397 absPath, err := filepath.Abs("testdata")
1398 if err != nil {
1399 t.Fatal("get abs path:", err)
1400 }
1401
1402 test("RelativePath", newfs("testdata"))
1403 test("AbsolutePath", newfs(absPath))
1404 }
1405
1406 func TestFileServerCleanPath(t *testing.T) {
1407 tests := []struct {
1408 path string
1409 wantCode int
1410 wantOpen []string
1411 }{
1412 {"/", 200, []string{"/", "/index.html"}},
1413 {"/dir", 301, []string{"/dir"}},
1414 {"/dir/", 200, []string{"/dir", "/dir/index.html"}},
1415 }
1416 for _, tt := range tests {
1417 var log []string
1418 rr := httptest.NewRecorder()
1419 req, _ := NewRequest("GET", "http://foo.localhost"+tt.path, nil)
1420 FileServer(fileServerCleanPathDir{&log}).ServeHTTP(rr, req)
1421 if !reflect.DeepEqual(log, tt.wantOpen) {
1422 t.Logf("For %s: Opens = %q; want %q", tt.path, log, tt.wantOpen)
1423 }
1424 if rr.Code != tt.wantCode {
1425 t.Logf("For %s: Response code = %d; want %d", tt.path, rr.Code, tt.wantCode)
1426 }
1427 }
1428 }
1429
1430 type fileServerCleanPathDir struct {
1431 log *[]string
1432 }
1433
1434 func (d fileServerCleanPathDir) Open(path string) (File, error) {
1435 *(d.log) = append(*(d.log), path)
1436 if path == "/" || path == "/dir" || path == "/dir/" {
1437
1438 return Dir(".").Open(".")
1439 }
1440 return nil, fs.ErrNotExist
1441 }
1442
1443 type panicOnSeek struct{ io.ReadSeeker }
1444
1445 func Test_scanETag(t *testing.T) {
1446 tests := []struct {
1447 in string
1448 wantETag string
1449 wantRemain string
1450 }{
1451 {`W/"etag-1"`, `W/"etag-1"`, ""},
1452 {`"etag-2"`, `"etag-2"`, ""},
1453 {`"etag-1", "etag-2"`, `"etag-1"`, `, "etag-2"`},
1454 {"", "", ""},
1455 {"W/", "", ""},
1456 {`W/"truc`, "", ""},
1457 {`w/"case-sensitive"`, "", ""},
1458 {`"spaced etag"`, "", ""},
1459 }
1460 for _, test := range tests {
1461 etag, remain := ExportScanETag(test.in)
1462 if etag != test.wantETag || remain != test.wantRemain {
1463 t.Errorf("scanETag(%q)=%q %q, want %q %q", test.in, etag, remain, test.wantETag, test.wantRemain)
1464 }
1465 }
1466 }
1467
1468
1469
1470 func TestServeFileRejectsInvalidSuffixLengths(t *testing.T) {
1471 run(t, testServeFileRejectsInvalidSuffixLengths, []testMode{http1Mode, https1Mode, http2Mode})
1472 }
1473 func testServeFileRejectsInvalidSuffixLengths(t *testing.T, mode testMode) {
1474 cst := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts
1475
1476 tests := []struct {
1477 r string
1478 wantCode int
1479 wantBody string
1480 }{
1481 {"bytes=--6", 416, "invalid range\n"},
1482 {"bytes=--0", 416, "invalid range\n"},
1483 {"bytes=---0", 416, "invalid range\n"},
1484 {"bytes=-6", 206, "hello\n"},
1485 {"bytes=6-", 206, "html says hello\n"},
1486 {"bytes=-6-", 416, "invalid range\n"},
1487 {"bytes=-0", 206, ""},
1488 {"bytes=", 200, "index.html says hello\n"},
1489 }
1490
1491 for _, tt := range tests {
1492 tt := tt
1493 t.Run(tt.r, func(t *testing.T) {
1494 req, err := NewRequest("GET", cst.URL+"/index.html", nil)
1495 if err != nil {
1496 t.Fatal(err)
1497 }
1498 req.Header.Set("Range", tt.r)
1499 res, err := cst.Client().Do(req)
1500 if err != nil {
1501 t.Fatal(err)
1502 }
1503 if g, w := res.StatusCode, tt.wantCode; g != w {
1504 t.Errorf("StatusCode mismatch: got %d want %d", g, w)
1505 }
1506 slurp, err := io.ReadAll(res.Body)
1507 res.Body.Close()
1508 if err != nil {
1509 t.Fatal(err)
1510 }
1511 if g, w := string(slurp), tt.wantBody; g != w {
1512 t.Fatalf("Content mismatch:\nGot: %q\nWant: %q", g, w)
1513 }
1514 })
1515 }
1516 }
1517
1518 func TestFileServerMethods(t *testing.T) {
1519 run(t, testFileServerMethods)
1520 }
1521 func testFileServerMethods(t *testing.T, mode testMode) {
1522 ts := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts
1523
1524 file, err := os.ReadFile(testFile)
1525 if err != nil {
1526 t.Fatal("reading file:", err)
1527 }
1528
1529
1530
1531
1532
1533 for _, method := range []string{
1534 MethodGet,
1535 MethodHead,
1536 MethodPost,
1537 MethodPut,
1538 MethodPatch,
1539 MethodDelete,
1540 MethodOptions,
1541 MethodTrace,
1542 } {
1543 req, _ := NewRequest(method, ts.URL+"/file", nil)
1544 t.Log(req.URL)
1545 res, err := ts.Client().Do(req)
1546 if err != nil {
1547 t.Fatal(err)
1548 }
1549 body, err := io.ReadAll(res.Body)
1550 res.Body.Close()
1551 if err != nil {
1552 t.Fatal(err)
1553 }
1554 wantBody := file
1555 if method == MethodHead {
1556 wantBody = nil
1557 }
1558 if !bytes.Equal(body, wantBody) {
1559 t.Fatalf("%v: got body %q, want %q", method, body, wantBody)
1560 }
1561 if got, want := res.Header.Get("Content-Length"), fmt.Sprint(len(file)); got != want {
1562 t.Fatalf("%v: got Content-Length %q, want %q", method, got, want)
1563 }
1564 }
1565 }
1566
1567 func TestFileServerFS(t *testing.T) {
1568 filename := "index.html"
1569 contents := []byte("index.html says hello")
1570 fsys := fstest.MapFS{
1571 filename: {Data: contents},
1572 }
1573 ts := newClientServerTest(t, http1Mode, FileServerFS(fsys)).ts
1574 defer ts.Close()
1575
1576 res, err := ts.Client().Get(ts.URL + "/" + filename)
1577 if err != nil {
1578 t.Fatal(err)
1579 }
1580 b, err := io.ReadAll(res.Body)
1581 if err != nil {
1582 t.Fatal("reading Body:", err)
1583 }
1584 if s := string(b); s != string(contents) {
1585 t.Errorf("for path %q got %q, want %q", filename, s, contents)
1586 }
1587 res.Body.Close()
1588 }
1589
1590 func TestServeFileFS(t *testing.T) {
1591 filename := "index.html"
1592 contents := []byte("index.html says hello")
1593 fsys := fstest.MapFS{
1594 filename: {Data: contents},
1595 }
1596 ts := newClientServerTest(t, http1Mode, HandlerFunc(func(w ResponseWriter, r *Request) {
1597 ServeFileFS(w, r, fsys, filename)
1598 })).ts
1599 defer ts.Close()
1600
1601 res, err := ts.Client().Get(ts.URL + "/" + filename)
1602 if err != nil {
1603 t.Fatal(err)
1604 }
1605 b, err := io.ReadAll(res.Body)
1606 if err != nil {
1607 t.Fatal("reading Body:", err)
1608 }
1609 if s := string(b); s != string(contents) {
1610 t.Errorf("for path %q got %q, want %q", filename, s, contents)
1611 }
1612 res.Body.Close()
1613 }
1614
1615 func TestServeFileZippingResponseWriter(t *testing.T) {
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629 filename := "index.html"
1630 contents := []byte("contents will be sent with Content-Encoding: gzip")
1631 fsys := fstest.MapFS{
1632 filename: {Data: contents},
1633 }
1634 ts := newClientServerTest(t, http1Mode, HandlerFunc(func(w ResponseWriter, r *Request) {
1635 w.Header().Set("Content-Encoding", "gzip")
1636 gzw := gzip.NewWriter(w)
1637 defer gzw.Close()
1638 ServeFileFS(gzipResponseWriter{w: gzw, ResponseWriter: w}, r, fsys, filename)
1639 })).ts
1640 defer ts.Close()
1641
1642 res, err := ts.Client().Get(ts.URL + "/" + filename)
1643 if err != nil {
1644 t.Fatal(err)
1645 }
1646 b, err := io.ReadAll(res.Body)
1647 if err != nil {
1648 t.Fatal("reading Body:", err)
1649 }
1650 if s := string(b); s != string(contents) {
1651 t.Errorf("for path %q got %q, want %q", filename, s, contents)
1652 }
1653 res.Body.Close()
1654 }
1655
1656 type gzipResponseWriter struct {
1657 ResponseWriter
1658 w *gzip.Writer
1659 }
1660
1661 func (grw gzipResponseWriter) Write(b []byte) (int, error) {
1662 return grw.w.Write(b)
1663 }
1664
1665 func (grw gzipResponseWriter) Flush() {
1666 grw.w.Flush()
1667 if fw, ok := grw.ResponseWriter.(http.Flusher); ok {
1668 fw.Flush()
1669 }
1670 }
1671
View as plain text