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