github.com/aloncn/graphics-go@v0.0.1/src/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 "strconv" 28 "strings" 29 "testing" 30 "time" 31 ) 32 33 const ( 34 testFile = "testdata/file" 35 testFileLen = 11 36 ) 37 38 type wantRange struct { 39 start, end int64 // range [start,end) 40 } 41 42 var itoa = strconv.Itoa 43 44 var ServeFileRangeTests = []struct { 45 r string 46 code int 47 ranges []wantRange 48 }{ 49 {r: "", code: StatusOK}, 50 {r: "bytes=0-4", code: StatusPartialContent, ranges: []wantRange{{0, 5}}}, 51 {r: "bytes=2-", code: StatusPartialContent, ranges: []wantRange{{2, testFileLen}}}, 52 {r: "bytes=-5", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 5, testFileLen}}}, 53 {r: "bytes=3-7", code: StatusPartialContent, ranges: []wantRange{{3, 8}}}, 54 {r: "bytes=0-0,-2", code: StatusPartialContent, ranges: []wantRange{{0, 1}, {testFileLen - 2, testFileLen}}}, 55 {r: "bytes=0-1,5-8", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, 9}}}, 56 {r: "bytes=0-1,5-", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, testFileLen}}}, 57 {r: "bytes=5-1000", code: StatusPartialContent, ranges: []wantRange{{5, testFileLen}}}, 58 {r: "bytes=0-,1-,2-,3-,4-", code: StatusOK}, // ignore wasteful range request 59 {r: "bytes=0-9", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen - 1}}}, 60 {r: "bytes=0-10", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}}, 61 {r: "bytes=0-11", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}}, 62 {r: "bytes=10-11", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}}, 63 {r: "bytes=10-", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}}, 64 {r: "bytes=11-", code: StatusRequestedRangeNotSatisfiable}, 65 {r: "bytes=11-12", code: StatusRequestedRangeNotSatisfiable}, 66 {r: "bytes=12-12", code: StatusRequestedRangeNotSatisfiable}, 67 {r: "bytes=11-100", code: StatusRequestedRangeNotSatisfiable}, 68 {r: "bytes=12-100", code: StatusRequestedRangeNotSatisfiable}, 69 {r: "bytes=100-", code: StatusRequestedRangeNotSatisfiable}, 70 {r: "bytes=100-1000", code: StatusRequestedRangeNotSatisfiable}, 71 } 72 73 func TestServeFile(t *testing.T) { 74 defer afterTest(t) 75 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 76 ServeFile(w, r, "testdata/file") 77 })) 78 defer ts.Close() 79 80 var err error 81 82 file, err := ioutil.ReadFile(testFile) 83 if err != nil { 84 t.Fatal("reading file:", err) 85 } 86 87 // set up the Request (re-used for all tests) 88 var req Request 89 req.Header = make(Header) 90 if req.URL, err = url.Parse(ts.URL); err != nil { 91 t.Fatal("ParseURL:", err) 92 } 93 req.Method = "GET" 94 95 // straight GET 96 _, body := getBody(t, "straight get", req) 97 if !bytes.Equal(body, file) { 98 t.Fatalf("body mismatch: got %q, want %q", body, file) 99 } 100 101 // Range tests 102 Cases: 103 for _, rt := range ServeFileRangeTests { 104 if rt.r != "" { 105 req.Header.Set("Range", rt.r) 106 } 107 resp, body := getBody(t, fmt.Sprintf("range test %q", rt.r), req) 108 if resp.StatusCode != rt.code { 109 t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, resp.StatusCode, rt.code) 110 } 111 if rt.code == StatusRequestedRangeNotSatisfiable { 112 continue 113 } 114 wantContentRange := "" 115 if len(rt.ranges) == 1 { 116 rng := rt.ranges[0] 117 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen) 118 } 119 cr := resp.Header.Get("Content-Range") 120 if cr != wantContentRange { 121 t.Errorf("range=%q: Content-Range = %q, want %q", rt.r, cr, wantContentRange) 122 } 123 ct := resp.Header.Get("Content-Type") 124 if len(rt.ranges) == 1 { 125 rng := rt.ranges[0] 126 wantBody := file[rng.start:rng.end] 127 if !bytes.Equal(body, wantBody) { 128 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody) 129 } 130 if strings.HasPrefix(ct, "multipart/byteranges") { 131 t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct) 132 } 133 } 134 if len(rt.ranges) > 1 { 135 typ, params, err := mime.ParseMediaType(ct) 136 if err != nil { 137 t.Errorf("range=%q content-type = %q; %v", rt.r, ct, err) 138 continue 139 } 140 if typ != "multipart/byteranges" { 141 t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ) 142 continue 143 } 144 if params["boundary"] == "" { 145 t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct) 146 continue 147 } 148 if g, w := resp.ContentLength, int64(len(body)); g != w { 149 t.Errorf("range=%q Content-Length = %d; want %d", rt.r, g, w) 150 continue 151 } 152 mr := multipart.NewReader(bytes.NewReader(body), params["boundary"]) 153 for ri, rng := range rt.ranges { 154 part, err := mr.NextPart() 155 if err != nil { 156 t.Errorf("range=%q, reading part index %d: %v", rt.r, ri, err) 157 continue Cases 158 } 159 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen) 160 if g, w := part.Header.Get("Content-Range"), wantContentRange; g != w { 161 t.Errorf("range=%q: part Content-Range = %q; want %q", rt.r, g, w) 162 } 163 body, err := ioutil.ReadAll(part) 164 if err != nil { 165 t.Errorf("range=%q, reading part index %d body: %v", rt.r, ri, err) 166 continue Cases 167 } 168 wantBody := file[rng.start:rng.end] 169 if !bytes.Equal(body, wantBody) { 170 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody) 171 } 172 } 173 _, err = mr.NextPart() 174 if err != io.EOF { 175 t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err) 176 } 177 } 178 } 179 } 180 181 func TestServeFile_DotDot(t *testing.T) { 182 tests := []struct { 183 req string 184 wantStatus int 185 }{ 186 {"/testdata/file", 200}, 187 {"/../file", 400}, 188 {"/..", 400}, 189 {"/../", 400}, 190 {"/../foo", 400}, 191 {"/..\\foo", 400}, 192 {"/file/a", 200}, 193 {"/file/a..", 200}, 194 {"/file/a/..", 400}, 195 {"/file/a\\..", 400}, 196 } 197 for _, tt := range tests { 198 req, err := ReadRequest(bufio.NewReader(strings.NewReader("GET " + tt.req + " HTTP/1.1\r\nHost: foo\r\n\r\n"))) 199 if err != nil { 200 t.Errorf("bad request %q: %v", tt.req, err) 201 continue 202 } 203 rec := httptest.NewRecorder() 204 ServeFile(rec, req, "testdata/file") 205 if rec.Code != tt.wantStatus { 206 t.Errorf("for request %q, status = %d; want %d", tt.req, rec.Code, tt.wantStatus) 207 } 208 } 209 } 210 211 var fsRedirectTestData = []struct { 212 original, redirect string 213 }{ 214 {"/test/index.html", "/test/"}, 215 {"/test/testdata", "/test/testdata/"}, 216 {"/test/testdata/file/", "/test/testdata/file"}, 217 } 218 219 func TestFSRedirect(t *testing.T) { 220 defer afterTest(t) 221 ts := httptest.NewServer(StripPrefix("/test", FileServer(Dir(".")))) 222 defer ts.Close() 223 224 for _, data := range fsRedirectTestData { 225 res, err := Get(ts.URL + data.original) 226 if err != nil { 227 t.Fatal(err) 228 } 229 res.Body.Close() 230 if g, e := res.Request.URL.Path, data.redirect; g != e { 231 t.Errorf("redirect from %s: got %s, want %s", data.original, g, e) 232 } 233 } 234 } 235 236 type testFileSystem struct { 237 open func(name string) (File, error) 238 } 239 240 func (fs *testFileSystem) Open(name string) (File, error) { 241 return fs.open(name) 242 } 243 244 func TestFileServerCleans(t *testing.T) { 245 defer afterTest(t) 246 ch := make(chan string, 1) 247 fs := FileServer(&testFileSystem{func(name string) (File, error) { 248 ch <- name 249 return nil, errors.New("file does not exist") 250 }}) 251 tests := []struct { 252 reqPath, openArg string 253 }{ 254 {"/foo.txt", "/foo.txt"}, 255 {"//foo.txt", "/foo.txt"}, 256 {"/../foo.txt", "/foo.txt"}, 257 } 258 req, _ := NewRequest("GET", "http://example.com", nil) 259 for n, test := range tests { 260 rec := httptest.NewRecorder() 261 req.URL.Path = test.reqPath 262 fs.ServeHTTP(rec, req) 263 if got := <-ch; got != test.openArg { 264 t.Errorf("test %d: got %q, want %q", n, got, test.openArg) 265 } 266 } 267 } 268 269 func TestFileServerEscapesNames(t *testing.T) { 270 defer afterTest(t) 271 const dirListPrefix = "<pre>\n" 272 const dirListSuffix = "\n</pre>\n" 273 tests := []struct { 274 name, escaped string 275 }{ 276 {`simple_name`, `<a href="simple_name">simple_name</a>`}, 277 {`"'<>&`, `<a href="%22%27%3C%3E&">"'<>&</a>`}, 278 {`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`}, 279 {`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo"><combo>?foo</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 // Tests that ServeFile doesn't add a Content-Length if a Content-Encoding is 512 // specified. 513 func TestServeFileWithContentEncoding_h1(t *testing.T) { testServeFileWithContentEncoding(t, h1Mode) } 514 func TestServeFileWithContentEncoding_h2(t *testing.T) { testServeFileWithContentEncoding(t, h2Mode) } 515 func testServeFileWithContentEncoding(t *testing.T, h2 bool) { 516 defer afterTest(t) 517 cst := newClientServerTest(t, h2, HandlerFunc(func(w ResponseWriter, r *Request) { 518 w.Header().Set("Content-Encoding", "foo") 519 ServeFile(w, r, "testdata/file") 520 521 // Because the testdata is so small, it would fit in 522 // both the h1 and h2 Server's write buffers. For h1, 523 // sendfile is used, though, forcing a header flush at 524 // the io.Copy. http2 doesn't do a header flush so 525 // buffers all 11 bytes and then adds its own 526 // Content-Length. To prevent the Server's 527 // Content-Length and test ServeFile only, flush here. 528 w.(Flusher).Flush() 529 })) 530 defer cst.close() 531 resp, err := cst.c.Get(cst.ts.URL) 532 if err != nil { 533 t.Fatal(err) 534 } 535 resp.Body.Close() 536 if g, e := resp.ContentLength, int64(-1); g != e { 537 t.Errorf("Content-Length mismatch: got %d, want %d", g, e) 538 } 539 } 540 541 func TestServeIndexHtml(t *testing.T) { 542 defer afterTest(t) 543 const want = "index.html says hello\n" 544 ts := httptest.NewServer(FileServer(Dir("."))) 545 defer ts.Close() 546 547 for _, path := range []string{"/testdata/", "/testdata/index.html"} { 548 res, err := Get(ts.URL + path) 549 if err != nil { 550 t.Fatal(err) 551 } 552 b, err := ioutil.ReadAll(res.Body) 553 if err != nil { 554 t.Fatal("reading Body:", err) 555 } 556 if s := string(b); s != want { 557 t.Errorf("for path %q got %q, want %q", path, s, want) 558 } 559 res.Body.Close() 560 } 561 } 562 563 func TestFileServerZeroByte(t *testing.T) { 564 defer afterTest(t) 565 ts := httptest.NewServer(FileServer(Dir("."))) 566 defer ts.Close() 567 568 res, err := Get(ts.URL + "/..\x00") 569 if err != nil { 570 t.Fatal(err) 571 } 572 b, err := ioutil.ReadAll(res.Body) 573 if err != nil { 574 t.Fatal("reading Body:", err) 575 } 576 if res.StatusCode == 200 { 577 t.Errorf("got status 200; want an error. Body is:\n%s", string(b)) 578 } 579 } 580 581 type fakeFileInfo struct { 582 dir bool 583 basename string 584 modtime time.Time 585 ents []*fakeFileInfo 586 contents string 587 err error 588 } 589 590 func (f *fakeFileInfo) Name() string { return f.basename } 591 func (f *fakeFileInfo) Sys() interface{} { return nil } 592 func (f *fakeFileInfo) ModTime() time.Time { return f.modtime } 593 func (f *fakeFileInfo) IsDir() bool { return f.dir } 594 func (f *fakeFileInfo) Size() int64 { return int64(len(f.contents)) } 595 func (f *fakeFileInfo) Mode() os.FileMode { 596 if f.dir { 597 return 0755 | os.ModeDir 598 } 599 return 0644 600 } 601 602 type fakeFile struct { 603 io.ReadSeeker 604 fi *fakeFileInfo 605 path string // as opened 606 entpos int 607 } 608 609 func (f *fakeFile) Close() error { return nil } 610 func (f *fakeFile) Stat() (os.FileInfo, error) { return f.fi, nil } 611 func (f *fakeFile) Readdir(count int) ([]os.FileInfo, error) { 612 if !f.fi.dir { 613 return nil, os.ErrInvalid 614 } 615 var fis []os.FileInfo 616 617 limit := f.entpos + count 618 if count <= 0 || limit > len(f.fi.ents) { 619 limit = len(f.fi.ents) 620 } 621 for ; f.entpos < limit; f.entpos++ { 622 fis = append(fis, f.fi.ents[f.entpos]) 623 } 624 625 if len(fis) == 0 && count > 0 { 626 return fis, io.EOF 627 } else { 628 return fis, nil 629 } 630 } 631 632 type fakeFS map[string]*fakeFileInfo 633 634 func (fs fakeFS) Open(name string) (File, error) { 635 name = path.Clean(name) 636 f, ok := fs[name] 637 if !ok { 638 return nil, os.ErrNotExist 639 } 640 if f.err != nil { 641 return nil, f.err 642 } 643 return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil 644 } 645 646 func TestDirectoryIfNotModified(t *testing.T) { 647 defer afterTest(t) 648 const indexContents = "I am a fake index.html file" 649 fileMod := time.Unix(1000000000, 0).UTC() 650 fileModStr := fileMod.Format(TimeFormat) 651 dirMod := time.Unix(123, 0).UTC() 652 indexFile := &fakeFileInfo{ 653 basename: "index.html", 654 modtime: fileMod, 655 contents: indexContents, 656 } 657 fs := fakeFS{ 658 "/": &fakeFileInfo{ 659 dir: true, 660 modtime: dirMod, 661 ents: []*fakeFileInfo{indexFile}, 662 }, 663 "/index.html": indexFile, 664 } 665 666 ts := httptest.NewServer(FileServer(fs)) 667 defer ts.Close() 668 669 res, err := Get(ts.URL) 670 if err != nil { 671 t.Fatal(err) 672 } 673 b, err := ioutil.ReadAll(res.Body) 674 if err != nil { 675 t.Fatal(err) 676 } 677 if string(b) != indexContents { 678 t.Fatalf("Got body %q; want %q", b, indexContents) 679 } 680 res.Body.Close() 681 682 lastMod := res.Header.Get("Last-Modified") 683 if lastMod != fileModStr { 684 t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr) 685 } 686 687 req, _ := NewRequest("GET", ts.URL, nil) 688 req.Header.Set("If-Modified-Since", lastMod) 689 690 res, err = DefaultClient.Do(req) 691 if err != nil { 692 t.Fatal(err) 693 } 694 if res.StatusCode != 304 { 695 t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode) 696 } 697 res.Body.Close() 698 699 // Advance the index.html file's modtime, but not the directory's. 700 indexFile.modtime = indexFile.modtime.Add(1 * time.Hour) 701 702 res, err = DefaultClient.Do(req) 703 if err != nil { 704 t.Fatal(err) 705 } 706 if res.StatusCode != 200 { 707 t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res) 708 } 709 res.Body.Close() 710 } 711 712 func mustStat(t *testing.T, fileName string) os.FileInfo { 713 fi, err := os.Stat(fileName) 714 if err != nil { 715 t.Fatal(err) 716 } 717 return fi 718 } 719 720 func TestServeContent(t *testing.T) { 721 defer afterTest(t) 722 type serveParam struct { 723 name string 724 modtime time.Time 725 content io.ReadSeeker 726 contentType string 727 etag string 728 } 729 servec := make(chan serveParam, 1) 730 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 731 p := <-servec 732 if p.etag != "" { 733 w.Header().Set("ETag", p.etag) 734 } 735 if p.contentType != "" { 736 w.Header().Set("Content-Type", p.contentType) 737 } 738 ServeContent(w, r, p.name, p.modtime, p.content) 739 })) 740 defer ts.Close() 741 742 type testCase struct { 743 // One of file or content must be set: 744 file string 745 content io.ReadSeeker 746 747 modtime time.Time 748 serveETag string // optional 749 serveContentType string // optional 750 reqHeader map[string]string 751 wantLastMod string 752 wantContentType string 753 wantStatus int 754 } 755 htmlModTime := mustStat(t, "testdata/index.html").ModTime() 756 tests := map[string]testCase{ 757 "no_last_modified": { 758 file: "testdata/style.css", 759 wantContentType: "text/css; charset=utf-8", 760 wantStatus: 200, 761 }, 762 "with_last_modified": { 763 file: "testdata/index.html", 764 wantContentType: "text/html; charset=utf-8", 765 modtime: htmlModTime, 766 wantLastMod: htmlModTime.UTC().Format(TimeFormat), 767 wantStatus: 200, 768 }, 769 "not_modified_modtime": { 770 file: "testdata/style.css", 771 modtime: htmlModTime, 772 reqHeader: map[string]string{ 773 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat), 774 }, 775 wantStatus: 304, 776 }, 777 "not_modified_modtime_with_contenttype": { 778 file: "testdata/style.css", 779 serveContentType: "text/css", // explicit content type 780 modtime: htmlModTime, 781 reqHeader: map[string]string{ 782 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat), 783 }, 784 wantStatus: 304, 785 }, 786 "not_modified_etag": { 787 file: "testdata/style.css", 788 serveETag: `"foo"`, 789 reqHeader: map[string]string{ 790 "If-None-Match": `"foo"`, 791 }, 792 wantStatus: 304, 793 }, 794 "not_modified_etag_no_seek": { 795 content: panicOnSeek{nil}, // should never be called 796 serveETag: `"foo"`, 797 reqHeader: map[string]string{ 798 "If-None-Match": `"foo"`, 799 }, 800 wantStatus: 304, 801 }, 802 "range_good": { 803 file: "testdata/style.css", 804 serveETag: `"A"`, 805 reqHeader: map[string]string{ 806 "Range": "bytes=0-4", 807 }, 808 wantStatus: StatusPartialContent, 809 wantContentType: "text/css; charset=utf-8", 810 }, 811 // An If-Range resource for entity "A", but entity "B" is now current. 812 // The Range request should be ignored. 813 "range_no_match": { 814 file: "testdata/style.css", 815 serveETag: `"A"`, 816 reqHeader: map[string]string{ 817 "Range": "bytes=0-4", 818 "If-Range": `"B"`, 819 }, 820 wantStatus: 200, 821 wantContentType: "text/css; charset=utf-8", 822 }, 823 "range_with_modtime": { 824 file: "testdata/style.css", 825 modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC), 826 reqHeader: map[string]string{ 827 "Range": "bytes=0-4", 828 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT", 829 }, 830 wantStatus: StatusPartialContent, 831 wantContentType: "text/css; charset=utf-8", 832 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT", 833 }, 834 "range_with_modtime_nanos": { 835 file: "testdata/style.css", 836 modtime: time.Date(2014, 6, 25, 17, 12, 18, 123 /* nanos */, time.UTC), 837 reqHeader: map[string]string{ 838 "Range": "bytes=0-4", 839 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT", 840 }, 841 wantStatus: StatusPartialContent, 842 wantContentType: "text/css; charset=utf-8", 843 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT", 844 }, 845 "unix_zero_modtime": { 846 content: strings.NewReader("<html>foo"), 847 modtime: time.Unix(0, 0), 848 wantStatus: StatusOK, 849 wantContentType: "text/html; charset=utf-8", 850 }, 851 } 852 for testName, tt := range tests { 853 var content io.ReadSeeker 854 if tt.file != "" { 855 f, err := os.Open(tt.file) 856 if err != nil { 857 t.Fatalf("test %q: %v", testName, err) 858 } 859 defer f.Close() 860 content = f 861 } else { 862 content = tt.content 863 } 864 865 servec <- serveParam{ 866 name: filepath.Base(tt.file), 867 content: content, 868 modtime: tt.modtime, 869 etag: tt.serveETag, 870 contentType: tt.serveContentType, 871 } 872 req, err := NewRequest("GET", ts.URL, nil) 873 if err != nil { 874 t.Fatal(err) 875 } 876 for k, v := range tt.reqHeader { 877 req.Header.Set(k, v) 878 } 879 res, err := DefaultClient.Do(req) 880 if err != nil { 881 t.Fatal(err) 882 } 883 io.Copy(ioutil.Discard, res.Body) 884 res.Body.Close() 885 if res.StatusCode != tt.wantStatus { 886 t.Errorf("test %q: status = %d; want %d", testName, res.StatusCode, tt.wantStatus) 887 } 888 if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e { 889 t.Errorf("test %q: content-type = %q, want %q", testName, g, e) 890 } 891 if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e { 892 t.Errorf("test %q: last-modified = %q, want %q", testName, g, e) 893 } 894 } 895 } 896 897 // Issue 12991 898 func TestServerFileStatError(t *testing.T) { 899 rec := httptest.NewRecorder() 900 r, _ := NewRequest("GET", "http://foo/", nil) 901 redirect := false 902 name := "file.txt" 903 fs := issue12991FS{} 904 ExportServeFile(rec, r, fs, name, redirect) 905 if body := rec.Body.String(); !strings.Contains(body, "403") || !strings.Contains(body, "Forbidden") { 906 t.Errorf("wanted 403 forbidden message; got: %s", body) 907 } 908 } 909 910 type issue12991FS struct{} 911 912 func (issue12991FS) Open(string) (File, error) { return issue12991File{}, nil } 913 914 type issue12991File struct{ File } 915 916 func (issue12991File) Stat() (os.FileInfo, error) { return nil, os.ErrPermission } 917 func (issue12991File) Close() error { return nil } 918 919 func TestServeContentErrorMessages(t *testing.T) { 920 defer afterTest(t) 921 fs := fakeFS{ 922 "/500": &fakeFileInfo{ 923 err: errors.New("random error"), 924 }, 925 "/403": &fakeFileInfo{ 926 err: &os.PathError{Err: os.ErrPermission}, 927 }, 928 } 929 ts := httptest.NewServer(FileServer(fs)) 930 defer ts.Close() 931 for _, code := range []int{403, 404, 500} { 932 res, err := DefaultClient.Get(fmt.Sprintf("%s/%d", ts.URL, code)) 933 if err != nil { 934 t.Errorf("Error fetching /%d: %v", code, err) 935 continue 936 } 937 if res.StatusCode != code { 938 t.Errorf("For /%d, status code = %d; want %d", code, res.StatusCode, code) 939 } 940 res.Body.Close() 941 } 942 } 943 944 // verifies that sendfile is being used on Linux 945 func TestLinuxSendfile(t *testing.T) { 946 defer afterTest(t) 947 if runtime.GOOS != "linux" { 948 t.Skip("skipping; linux-only test") 949 } 950 if _, err := exec.LookPath("strace"); err != nil { 951 t.Skip("skipping; strace not found in path") 952 } 953 954 ln, err := net.Listen("tcp", "127.0.0.1:0") 955 if err != nil { 956 t.Fatal(err) 957 } 958 lnf, err := ln.(*net.TCPListener).File() 959 if err != nil { 960 t.Fatal(err) 961 } 962 defer ln.Close() 963 964 syscalls := "sendfile,sendfile64" 965 switch runtime.GOARCH { 966 case "mips64", "mips64le": 967 // mips64 strace doesn't support sendfile64 and will error out 968 // if we specify that with `-e trace='. 969 syscalls = "sendfile" 970 } 971 972 var buf bytes.Buffer 973 child := exec.Command("strace", "-f", "-q", "-e", "trace="+syscalls, os.Args[0], "-test.run=TestLinuxSendfileChild") 974 child.ExtraFiles = append(child.ExtraFiles, lnf) 975 child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...) 976 child.Stdout = &buf 977 child.Stderr = &buf 978 if err := child.Start(); err != nil { 979 t.Skipf("skipping; failed to start straced child: %v", err) 980 } 981 982 res, err := Get(fmt.Sprintf("http://%s/", ln.Addr())) 983 if err != nil { 984 t.Fatalf("http client error: %v", err) 985 } 986 _, err = io.Copy(ioutil.Discard, res.Body) 987 if err != nil { 988 t.Fatalf("client body read error: %v", err) 989 } 990 res.Body.Close() 991 992 // Force child to exit cleanly. 993 Post(fmt.Sprintf("http://%s/quit", ln.Addr()), "", nil) 994 child.Wait() 995 996 rx := regexp.MustCompile(`sendfile(64)?\(\d+,\s*\d+,\s*NULL,\s*\d+\)\s*=\s*\d+\s*\n`) 997 rxResume := regexp.MustCompile(`<\.\.\. sendfile(64)? resumed> \)\s*=\s*\d+\s*\n`) 998 out := buf.String() 999 if !rx.MatchString(out) && !rxResume.MatchString(out) { 1000 t.Errorf("no sendfile system call found in:\n%s", out) 1001 } 1002 } 1003 1004 func getBody(t *testing.T, testName string, req Request) (*Response, []byte) { 1005 r, err := DefaultClient.Do(&req) 1006 if err != nil { 1007 t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err) 1008 } 1009 b, err := ioutil.ReadAll(r.Body) 1010 if err != nil { 1011 t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err) 1012 } 1013 return r, b 1014 } 1015 1016 // TestLinuxSendfileChild isn't a real test. It's used as a helper process 1017 // for TestLinuxSendfile. 1018 func TestLinuxSendfileChild(*testing.T) { 1019 if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { 1020 return 1021 } 1022 defer os.Exit(0) 1023 fd3 := os.NewFile(3, "ephemeral-port-listener") 1024 ln, err := net.FileListener(fd3) 1025 if err != nil { 1026 panic(err) 1027 } 1028 mux := NewServeMux() 1029 mux.Handle("/", FileServer(Dir("testdata"))) 1030 mux.HandleFunc("/quit", func(ResponseWriter, *Request) { 1031 os.Exit(0) 1032 }) 1033 s := &Server{Handler: mux} 1034 err = s.Serve(ln) 1035 if err != nil { 1036 panic(err) 1037 } 1038 } 1039 1040 func TestFileServerCleanPath(t *testing.T) { 1041 tests := []struct { 1042 path string 1043 wantCode int 1044 wantOpen []string 1045 }{ 1046 {"/", 200, []string{"/", "/index.html"}}, 1047 {"/dir", 301, []string{"/dir"}}, 1048 {"/dir/", 200, []string{"/dir", "/dir/index.html"}}, 1049 } 1050 for _, tt := range tests { 1051 var log []string 1052 rr := httptest.NewRecorder() 1053 req, _ := NewRequest("GET", "http://foo.localhost"+tt.path, nil) 1054 FileServer(fileServerCleanPathDir{&log}).ServeHTTP(rr, req) 1055 if !reflect.DeepEqual(log, tt.wantOpen) { 1056 t.Logf("For %s: Opens = %q; want %q", tt.path, log, tt.wantOpen) 1057 } 1058 if rr.Code != tt.wantCode { 1059 t.Logf("For %s: Response code = %d; want %d", tt.path, rr.Code, tt.wantCode) 1060 } 1061 } 1062 } 1063 1064 type fileServerCleanPathDir struct { 1065 log *[]string 1066 } 1067 1068 func (d fileServerCleanPathDir) Open(path string) (File, error) { 1069 *(d.log) = append(*(d.log), path) 1070 if path == "/" || path == "/dir" || path == "/dir/" { 1071 // Just return back something that's a directory. 1072 return Dir(".").Open(".") 1073 } 1074 return nil, os.ErrNotExist 1075 } 1076 1077 type panicOnSeek struct{ io.ReadSeeker }