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