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