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