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