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