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