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