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