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