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