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