github.com/sean-/go@v0.0.0-20151219100004-97f854cd7bb6/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 "bytes" 9 "errors" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "mime" 14 "mime/multipart" 15 "net" 16 . "net/http" 17 "net/http/httptest" 18 "net/url" 19 "os" 20 "os/exec" 21 "path" 22 "path/filepath" 23 "reflect" 24 "regexp" 25 "runtime" 26 "strconv" 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 itoa = strconv.Itoa 42 43 var ServeFileRangeTests = []struct { 44 r string 45 code int 46 ranges []wantRange 47 }{ 48 {r: "", code: StatusOK}, 49 {r: "bytes=0-4", code: StatusPartialContent, ranges: []wantRange{{0, 5}}}, 50 {r: "bytes=2-", code: StatusPartialContent, ranges: []wantRange{{2, testFileLen}}}, 51 {r: "bytes=-5", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 5, testFileLen}}}, 52 {r: "bytes=3-7", code: StatusPartialContent, ranges: []wantRange{{3, 8}}}, 53 {r: "bytes=0-0,-2", code: StatusPartialContent, ranges: []wantRange{{0, 1}, {testFileLen - 2, testFileLen}}}, 54 {r: "bytes=0-1,5-8", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, 9}}}, 55 {r: "bytes=0-1,5-", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, testFileLen}}}, 56 {r: "bytes=5-1000", code: StatusPartialContent, ranges: []wantRange{{5, testFileLen}}}, 57 {r: "bytes=0-,1-,2-,3-,4-", code: StatusOK}, // ignore wasteful range request 58 {r: "bytes=0-9", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen - 1}}}, 59 {r: "bytes=0-10", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}}, 60 {r: "bytes=0-11", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}}, 61 {r: "bytes=10-11", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}}, 62 {r: "bytes=10-", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}}, 63 {r: "bytes=11-", code: StatusRequestedRangeNotSatisfiable}, 64 {r: "bytes=11-12", code: StatusRequestedRangeNotSatisfiable}, 65 {r: "bytes=12-12", code: StatusRequestedRangeNotSatisfiable}, 66 {r: "bytes=11-100", code: StatusRequestedRangeNotSatisfiable}, 67 {r: "bytes=12-100", code: StatusRequestedRangeNotSatisfiable}, 68 {r: "bytes=100-", code: StatusRequestedRangeNotSatisfiable}, 69 {r: "bytes=100-1000", code: StatusRequestedRangeNotSatisfiable}, 70 } 71 72 func TestServeFile(t *testing.T) { 73 defer afterTest(t) 74 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 75 ServeFile(w, r, "testdata/file") 76 })) 77 defer ts.Close() 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) 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) 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 var fsRedirectTestData = []struct { 181 original, redirect string 182 }{ 183 {"/test/index.html", "/test/"}, 184 {"/test/testdata", "/test/testdata/"}, 185 {"/test/testdata/file/", "/test/testdata/file"}, 186 } 187 188 func TestFSRedirect(t *testing.T) { 189 defer afterTest(t) 190 ts := httptest.NewServer(StripPrefix("/test", FileServer(Dir(".")))) 191 defer ts.Close() 192 193 for _, data := range fsRedirectTestData { 194 res, err := Get(ts.URL + data.original) 195 if err != nil { 196 t.Fatal(err) 197 } 198 res.Body.Close() 199 if g, e := res.Request.URL.Path, data.redirect; g != e { 200 t.Errorf("redirect from %s: got %s, want %s", data.original, g, e) 201 } 202 } 203 } 204 205 type testFileSystem struct { 206 open func(name string) (File, error) 207 } 208 209 func (fs *testFileSystem) Open(name string) (File, error) { 210 return fs.open(name) 211 } 212 213 func TestFileServerCleans(t *testing.T) { 214 defer afterTest(t) 215 ch := make(chan string, 1) 216 fs := FileServer(&testFileSystem{func(name string) (File, error) { 217 ch <- name 218 return nil, errors.New("file does not exist") 219 }}) 220 tests := []struct { 221 reqPath, openArg string 222 }{ 223 {"/foo.txt", "/foo.txt"}, 224 {"//foo.txt", "/foo.txt"}, 225 {"/../foo.txt", "/foo.txt"}, 226 } 227 req, _ := NewRequest("GET", "http://example.com", nil) 228 for n, test := range tests { 229 rec := httptest.NewRecorder() 230 req.URL.Path = test.reqPath 231 fs.ServeHTTP(rec, req) 232 if got := <-ch; got != test.openArg { 233 t.Errorf("test %d: got %q, want %q", n, got, test.openArg) 234 } 235 } 236 } 237 238 func TestFileServerEscapesNames(t *testing.T) { 239 defer afterTest(t) 240 const dirListPrefix = "<pre>\n" 241 const dirListSuffix = "\n</pre>\n" 242 tests := []struct { 243 name, escaped string 244 }{ 245 {`simple_name`, `<a href="simple_name">simple_name</a>`}, 246 {`"'<>&`, `<a href="%22%27%3C%3E&">"'<>&</a>`}, 247 {`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`}, 248 {`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo"><combo>?foo</a>`}, 249 } 250 251 // We put each test file in its own directory in the fakeFS so we can look at it in isolation. 252 fs := make(fakeFS) 253 for i, test := range tests { 254 testFile := &fakeFileInfo{basename: test.name} 255 fs[fmt.Sprintf("/%d", i)] = &fakeFileInfo{ 256 dir: true, 257 modtime: time.Unix(1000000000, 0).UTC(), 258 ents: []*fakeFileInfo{testFile}, 259 } 260 fs[fmt.Sprintf("/%d/%s", i, test.name)] = testFile 261 } 262 263 ts := httptest.NewServer(FileServer(&fs)) 264 defer ts.Close() 265 for i, test := range tests { 266 url := fmt.Sprintf("%s/%d", ts.URL, i) 267 res, err := Get(url) 268 if err != nil { 269 t.Fatalf("test %q: Get: %v", test.name, err) 270 } 271 b, err := ioutil.ReadAll(res.Body) 272 if err != nil { 273 t.Fatalf("test %q: read Body: %v", test.name, err) 274 } 275 s := string(b) 276 if !strings.HasPrefix(s, dirListPrefix) || !strings.HasSuffix(s, dirListSuffix) { 277 t.Errorf("test %q: listing dir, full output is %q, want prefix %q and suffix %q", test.name, s, dirListPrefix, dirListSuffix) 278 } 279 if trimmed := strings.TrimSuffix(strings.TrimPrefix(s, dirListPrefix), dirListSuffix); trimmed != test.escaped { 280 t.Errorf("test %q: listing dir, filename escaped to %q, want %q", test.name, trimmed, test.escaped) 281 } 282 res.Body.Close() 283 } 284 } 285 286 func TestFileServerSortsNames(t *testing.T) { 287 defer afterTest(t) 288 const contents = "I am a fake file" 289 dirMod := time.Unix(123, 0).UTC() 290 fileMod := time.Unix(1000000000, 0).UTC() 291 fs := fakeFS{ 292 "/": &fakeFileInfo{ 293 dir: true, 294 modtime: dirMod, 295 ents: []*fakeFileInfo{ 296 { 297 basename: "b", 298 modtime: fileMod, 299 contents: contents, 300 }, 301 { 302 basename: "a", 303 modtime: fileMod, 304 contents: contents, 305 }, 306 }, 307 }, 308 } 309 310 ts := httptest.NewServer(FileServer(&fs)) 311 defer ts.Close() 312 313 res, err := Get(ts.URL) 314 if err != nil { 315 t.Fatalf("Get: %v", err) 316 } 317 defer res.Body.Close() 318 319 b, err := ioutil.ReadAll(res.Body) 320 if err != nil { 321 t.Fatalf("read Body: %v", err) 322 } 323 s := string(b) 324 if !strings.Contains(s, "<a href=\"a\">a</a>\n<a href=\"b\">b</a>") { 325 t.Errorf("output appears to be unsorted:\n%s", s) 326 } 327 } 328 329 func mustRemoveAll(dir string) { 330 err := os.RemoveAll(dir) 331 if err != nil { 332 panic(err) 333 } 334 } 335 336 func TestFileServerImplicitLeadingSlash(t *testing.T) { 337 defer afterTest(t) 338 tempDir, err := ioutil.TempDir("", "") 339 if err != nil { 340 t.Fatalf("TempDir: %v", err) 341 } 342 defer mustRemoveAll(tempDir) 343 if err := ioutil.WriteFile(filepath.Join(tempDir, "foo.txt"), []byte("Hello world"), 0644); err != nil { 344 t.Fatalf("WriteFile: %v", err) 345 } 346 ts := httptest.NewServer(StripPrefix("/bar/", FileServer(Dir(tempDir)))) 347 defer ts.Close() 348 get := func(suffix string) string { 349 res, err := Get(ts.URL + suffix) 350 if err != nil { 351 t.Fatalf("Get %s: %v", suffix, err) 352 } 353 b, err := ioutil.ReadAll(res.Body) 354 if err != nil { 355 t.Fatalf("ReadAll %s: %v", suffix, err) 356 } 357 res.Body.Close() 358 return string(b) 359 } 360 if s := get("/bar/"); !strings.Contains(s, ">foo.txt<") { 361 t.Logf("expected a directory listing with foo.txt, got %q", s) 362 } 363 if s := get("/bar/foo.txt"); s != "Hello world" { 364 t.Logf("expected %q, got %q", "Hello world", s) 365 } 366 } 367 368 func TestDirJoin(t *testing.T) { 369 if runtime.GOOS == "windows" { 370 t.Skip("skipping test on windows") 371 } 372 wfi, err := os.Stat("/etc/hosts") 373 if err != nil { 374 t.Skip("skipping test; no /etc/hosts file") 375 } 376 test := func(d Dir, name string) { 377 f, err := d.Open(name) 378 if err != nil { 379 t.Fatalf("open of %s: %v", name, err) 380 } 381 defer f.Close() 382 gfi, err := f.Stat() 383 if err != nil { 384 t.Fatalf("stat of %s: %v", name, err) 385 } 386 if !os.SameFile(gfi, wfi) { 387 t.Errorf("%s got different file", name) 388 } 389 } 390 test(Dir("/etc/"), "/hosts") 391 test(Dir("/etc/"), "hosts") 392 test(Dir("/etc/"), "../../../../hosts") 393 test(Dir("/etc"), "/hosts") 394 test(Dir("/etc"), "hosts") 395 test(Dir("/etc"), "../../../../hosts") 396 397 // Not really directories, but since we use this trick in 398 // ServeFile, test it: 399 test(Dir("/etc/hosts"), "") 400 test(Dir("/etc/hosts"), "/") 401 test(Dir("/etc/hosts"), "../") 402 } 403 404 func TestEmptyDirOpenCWD(t *testing.T) { 405 test := func(d Dir) { 406 name := "fs_test.go" 407 f, err := d.Open(name) 408 if err != nil { 409 t.Fatalf("open of %s: %v", name, err) 410 } 411 defer f.Close() 412 } 413 test(Dir("")) 414 test(Dir(".")) 415 test(Dir("./")) 416 } 417 418 func TestServeFileContentType(t *testing.T) { 419 defer afterTest(t) 420 const ctype = "icecream/chocolate" 421 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 422 switch r.FormValue("override") { 423 case "1": 424 w.Header().Set("Content-Type", ctype) 425 case "2": 426 // Explicitly inhibit sniffing. 427 w.Header()["Content-Type"] = []string{} 428 } 429 ServeFile(w, r, "testdata/file") 430 })) 431 defer ts.Close() 432 get := func(override string, want []string) { 433 resp, err := Get(ts.URL + "?override=" + override) 434 if err != nil { 435 t.Fatal(err) 436 } 437 if h := resp.Header["Content-Type"]; !reflect.DeepEqual(h, want) { 438 t.Errorf("Content-Type mismatch: got %v, want %v", h, want) 439 } 440 resp.Body.Close() 441 } 442 get("0", []string{"text/plain; charset=utf-8"}) 443 get("1", []string{ctype}) 444 get("2", nil) 445 } 446 447 func TestServeFileMimeType(t *testing.T) { 448 defer afterTest(t) 449 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 450 ServeFile(w, r, "testdata/style.css") 451 })) 452 defer ts.Close() 453 resp, err := Get(ts.URL) 454 if err != nil { 455 t.Fatal(err) 456 } 457 resp.Body.Close() 458 want := "text/css; charset=utf-8" 459 if h := resp.Header.Get("Content-Type"); h != want { 460 t.Errorf("Content-Type mismatch: got %q, want %q", h, want) 461 } 462 } 463 464 func TestServeFileFromCWD(t *testing.T) { 465 defer afterTest(t) 466 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 467 ServeFile(w, r, "fs_test.go") 468 })) 469 defer ts.Close() 470 r, err := Get(ts.URL) 471 if err != nil { 472 t.Fatal(err) 473 } 474 r.Body.Close() 475 if r.StatusCode != 200 { 476 t.Fatalf("expected 200 OK, got %s", r.Status) 477 } 478 } 479 480 // Tests that ServeFile doesn't add a Content-Length if a Content-Encoding is 481 // specified. 482 func TestServeFileWithContentEncoding_h1(t *testing.T) { testServeFileWithContentEncoding(t, h1Mode) } 483 func TestServeFileWithContentEncoding_h2(t *testing.T) { testServeFileWithContentEncoding(t, h2Mode) } 484 func testServeFileWithContentEncoding(t *testing.T, h2 bool) { 485 defer afterTest(t) 486 cst := newClientServerTest(t, h2, HandlerFunc(func(w ResponseWriter, r *Request) { 487 w.Header().Set("Content-Encoding", "foo") 488 ServeFile(w, r, "testdata/file") 489 490 // Because the testdata is so small, it would fit in 491 // both the h1 and h2 Server's write buffers. For h1, 492 // sendfile is used, though, forcing a header flush at 493 // the io.Copy. http2 doesn't do a header flush so 494 // buffers all 11 bytes and then adds its own 495 // Content-Length. To prevent the Server's 496 // Content-Length and test ServeFile only, flush here. 497 w.(Flusher).Flush() 498 })) 499 defer cst.close() 500 resp, err := cst.c.Get(cst.ts.URL) 501 if err != nil { 502 t.Fatal(err) 503 } 504 resp.Body.Close() 505 if g, e := resp.ContentLength, int64(-1); g != e { 506 t.Errorf("Content-Length mismatch: got %d, want %d", g, e) 507 } 508 } 509 510 func TestServeIndexHtml(t *testing.T) { 511 defer afterTest(t) 512 const want = "index.html says hello\n" 513 ts := httptest.NewServer(FileServer(Dir("."))) 514 defer ts.Close() 515 516 for _, path := range []string{"/testdata/", "/testdata/index.html"} { 517 res, err := Get(ts.URL + path) 518 if err != nil { 519 t.Fatal(err) 520 } 521 b, err := ioutil.ReadAll(res.Body) 522 if err != nil { 523 t.Fatal("reading Body:", err) 524 } 525 if s := string(b); s != want { 526 t.Errorf("for path %q got %q, want %q", path, s, want) 527 } 528 res.Body.Close() 529 } 530 } 531 532 func TestFileServerZeroByte(t *testing.T) { 533 defer afterTest(t) 534 ts := httptest.NewServer(FileServer(Dir("."))) 535 defer ts.Close() 536 537 res, err := Get(ts.URL + "/..\x00") 538 if err != nil { 539 t.Fatal(err) 540 } 541 b, err := ioutil.ReadAll(res.Body) 542 if err != nil { 543 t.Fatal("reading Body:", err) 544 } 545 if res.StatusCode == 200 { 546 t.Errorf("got status 200; want an error. Body is:\n%s", string(b)) 547 } 548 } 549 550 type fakeFileInfo struct { 551 dir bool 552 basename string 553 modtime time.Time 554 ents []*fakeFileInfo 555 contents string 556 err error 557 } 558 559 func (f *fakeFileInfo) Name() string { return f.basename } 560 func (f *fakeFileInfo) Sys() interface{} { return nil } 561 func (f *fakeFileInfo) ModTime() time.Time { return f.modtime } 562 func (f *fakeFileInfo) IsDir() bool { return f.dir } 563 func (f *fakeFileInfo) Size() int64 { return int64(len(f.contents)) } 564 func (f *fakeFileInfo) Mode() os.FileMode { 565 if f.dir { 566 return 0755 | os.ModeDir 567 } 568 return 0644 569 } 570 571 type fakeFile struct { 572 io.ReadSeeker 573 fi *fakeFileInfo 574 path string // as opened 575 entpos int 576 } 577 578 func (f *fakeFile) Close() error { return nil } 579 func (f *fakeFile) Stat() (os.FileInfo, error) { return f.fi, nil } 580 func (f *fakeFile) Readdir(count int) ([]os.FileInfo, error) { 581 if !f.fi.dir { 582 return nil, os.ErrInvalid 583 } 584 var fis []os.FileInfo 585 586 limit := f.entpos + count 587 if count <= 0 || limit > len(f.fi.ents) { 588 limit = len(f.fi.ents) 589 } 590 for ; f.entpos < limit; f.entpos++ { 591 fis = append(fis, f.fi.ents[f.entpos]) 592 } 593 594 if len(fis) == 0 && count > 0 { 595 return fis, io.EOF 596 } else { 597 return fis, nil 598 } 599 } 600 601 type fakeFS map[string]*fakeFileInfo 602 603 func (fs fakeFS) Open(name string) (File, error) { 604 name = path.Clean(name) 605 f, ok := fs[name] 606 if !ok { 607 return nil, os.ErrNotExist 608 } 609 if f.err != nil { 610 return nil, f.err 611 } 612 return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil 613 } 614 615 func TestDirectoryIfNotModified(t *testing.T) { 616 defer afterTest(t) 617 const indexContents = "I am a fake index.html file" 618 fileMod := time.Unix(1000000000, 0).UTC() 619 fileModStr := fileMod.Format(TimeFormat) 620 dirMod := time.Unix(123, 0).UTC() 621 indexFile := &fakeFileInfo{ 622 basename: "index.html", 623 modtime: fileMod, 624 contents: indexContents, 625 } 626 fs := fakeFS{ 627 "/": &fakeFileInfo{ 628 dir: true, 629 modtime: dirMod, 630 ents: []*fakeFileInfo{indexFile}, 631 }, 632 "/index.html": indexFile, 633 } 634 635 ts := httptest.NewServer(FileServer(fs)) 636 defer ts.Close() 637 638 res, err := Get(ts.URL) 639 if err != nil { 640 t.Fatal(err) 641 } 642 b, err := ioutil.ReadAll(res.Body) 643 if err != nil { 644 t.Fatal(err) 645 } 646 if string(b) != indexContents { 647 t.Fatalf("Got body %q; want %q", b, indexContents) 648 } 649 res.Body.Close() 650 651 lastMod := res.Header.Get("Last-Modified") 652 if lastMod != fileModStr { 653 t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr) 654 } 655 656 req, _ := NewRequest("GET", ts.URL, nil) 657 req.Header.Set("If-Modified-Since", lastMod) 658 659 res, err = DefaultClient.Do(req) 660 if err != nil { 661 t.Fatal(err) 662 } 663 if res.StatusCode != 304 { 664 t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode) 665 } 666 res.Body.Close() 667 668 // Advance the index.html file's modtime, but not the directory's. 669 indexFile.modtime = indexFile.modtime.Add(1 * time.Hour) 670 671 res, err = DefaultClient.Do(req) 672 if err != nil { 673 t.Fatal(err) 674 } 675 if res.StatusCode != 200 { 676 t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res) 677 } 678 res.Body.Close() 679 } 680 681 func mustStat(t *testing.T, fileName string) os.FileInfo { 682 fi, err := os.Stat(fileName) 683 if err != nil { 684 t.Fatal(err) 685 } 686 return fi 687 } 688 689 func TestServeContent(t *testing.T) { 690 defer afterTest(t) 691 type serveParam struct { 692 name string 693 modtime time.Time 694 content io.ReadSeeker 695 contentType string 696 etag string 697 } 698 servec := make(chan serveParam, 1) 699 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 700 p := <-servec 701 if p.etag != "" { 702 w.Header().Set("ETag", p.etag) 703 } 704 if p.contentType != "" { 705 w.Header().Set("Content-Type", p.contentType) 706 } 707 ServeContent(w, r, p.name, p.modtime, p.content) 708 })) 709 defer ts.Close() 710 711 type testCase struct { 712 // One of file or content must be set: 713 file string 714 content io.ReadSeeker 715 716 modtime time.Time 717 serveETag string // optional 718 serveContentType string // optional 719 reqHeader map[string]string 720 wantLastMod string 721 wantContentType string 722 wantStatus int 723 } 724 htmlModTime := mustStat(t, "testdata/index.html").ModTime() 725 tests := map[string]testCase{ 726 "no_last_modified": { 727 file: "testdata/style.css", 728 wantContentType: "text/css; charset=utf-8", 729 wantStatus: 200, 730 }, 731 "with_last_modified": { 732 file: "testdata/index.html", 733 wantContentType: "text/html; charset=utf-8", 734 modtime: htmlModTime, 735 wantLastMod: htmlModTime.UTC().Format(TimeFormat), 736 wantStatus: 200, 737 }, 738 "not_modified_modtime": { 739 file: "testdata/style.css", 740 modtime: htmlModTime, 741 reqHeader: map[string]string{ 742 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat), 743 }, 744 wantStatus: 304, 745 }, 746 "not_modified_modtime_with_contenttype": { 747 file: "testdata/style.css", 748 serveContentType: "text/css", // explicit content type 749 modtime: htmlModTime, 750 reqHeader: map[string]string{ 751 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat), 752 }, 753 wantStatus: 304, 754 }, 755 "not_modified_etag": { 756 file: "testdata/style.css", 757 serveETag: `"foo"`, 758 reqHeader: map[string]string{ 759 "If-None-Match": `"foo"`, 760 }, 761 wantStatus: 304, 762 }, 763 "not_modified_etag_no_seek": { 764 content: panicOnSeek{nil}, // should never be called 765 serveETag: `"foo"`, 766 reqHeader: map[string]string{ 767 "If-None-Match": `"foo"`, 768 }, 769 wantStatus: 304, 770 }, 771 "range_good": { 772 file: "testdata/style.css", 773 serveETag: `"A"`, 774 reqHeader: map[string]string{ 775 "Range": "bytes=0-4", 776 }, 777 wantStatus: StatusPartialContent, 778 wantContentType: "text/css; charset=utf-8", 779 }, 780 // An If-Range resource for entity "A", but entity "B" is now current. 781 // The Range request should be ignored. 782 "range_no_match": { 783 file: "testdata/style.css", 784 serveETag: `"A"`, 785 reqHeader: map[string]string{ 786 "Range": "bytes=0-4", 787 "If-Range": `"B"`, 788 }, 789 wantStatus: 200, 790 wantContentType: "text/css; charset=utf-8", 791 }, 792 "range_with_modtime": { 793 file: "testdata/style.css", 794 modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC), 795 reqHeader: map[string]string{ 796 "Range": "bytes=0-4", 797 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT", 798 }, 799 wantStatus: StatusPartialContent, 800 wantContentType: "text/css; charset=utf-8", 801 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT", 802 }, 803 "range_with_modtime_nanos": { 804 file: "testdata/style.css", 805 modtime: time.Date(2014, 6, 25, 17, 12, 18, 123 /* nanos */, time.UTC), 806 reqHeader: map[string]string{ 807 "Range": "bytes=0-4", 808 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT", 809 }, 810 wantStatus: StatusPartialContent, 811 wantContentType: "text/css; charset=utf-8", 812 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT", 813 }, 814 "unix_zero_modtime": { 815 content: strings.NewReader("<html>foo"), 816 modtime: time.Unix(0, 0), 817 wantStatus: StatusOK, 818 wantContentType: "text/html; charset=utf-8", 819 }, 820 } 821 for testName, tt := range tests { 822 var content io.ReadSeeker 823 if tt.file != "" { 824 f, err := os.Open(tt.file) 825 if err != nil { 826 t.Fatalf("test %q: %v", testName, err) 827 } 828 defer f.Close() 829 content = f 830 } else { 831 content = tt.content 832 } 833 834 servec <- serveParam{ 835 name: filepath.Base(tt.file), 836 content: content, 837 modtime: tt.modtime, 838 etag: tt.serveETag, 839 contentType: tt.serveContentType, 840 } 841 req, err := NewRequest("GET", ts.URL, nil) 842 if err != nil { 843 t.Fatal(err) 844 } 845 for k, v := range tt.reqHeader { 846 req.Header.Set(k, v) 847 } 848 res, err := DefaultClient.Do(req) 849 if err != nil { 850 t.Fatal(err) 851 } 852 io.Copy(ioutil.Discard, res.Body) 853 res.Body.Close() 854 if res.StatusCode != tt.wantStatus { 855 t.Errorf("test %q: status = %d; want %d", testName, res.StatusCode, tt.wantStatus) 856 } 857 if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e { 858 t.Errorf("test %q: content-type = %q, want %q", testName, g, e) 859 } 860 if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e { 861 t.Errorf("test %q: last-modified = %q, want %q", testName, g, e) 862 } 863 } 864 } 865 866 // Issue 12991 867 func TestServerFileStatError(t *testing.T) { 868 rec := httptest.NewRecorder() 869 r, _ := NewRequest("GET", "http://foo/", nil) 870 redirect := false 871 name := "file.txt" 872 fs := issue12991FS{} 873 ExportServeFile(rec, r, fs, name, redirect) 874 if body := rec.Body.String(); !strings.Contains(body, "403") || !strings.Contains(body, "Forbidden") { 875 t.Errorf("wanted 403 forbidden message; got: %s", body) 876 } 877 } 878 879 type issue12991FS struct{} 880 881 func (issue12991FS) Open(string) (File, error) { return issue12991File{}, nil } 882 883 type issue12991File struct{ File } 884 885 func (issue12991File) Stat() (os.FileInfo, error) { return nil, os.ErrPermission } 886 func (issue12991File) Close() error { return nil } 887 888 func TestServeContentErrorMessages(t *testing.T) { 889 defer afterTest(t) 890 fs := fakeFS{ 891 "/500": &fakeFileInfo{ 892 err: errors.New("random error"), 893 }, 894 "/403": &fakeFileInfo{ 895 err: &os.PathError{Err: os.ErrPermission}, 896 }, 897 } 898 ts := httptest.NewServer(FileServer(fs)) 899 defer ts.Close() 900 for _, code := range []int{403, 404, 500} { 901 res, err := DefaultClient.Get(fmt.Sprintf("%s/%d", ts.URL, code)) 902 if err != nil { 903 t.Errorf("Error fetching /%d: %v", code, err) 904 continue 905 } 906 if res.StatusCode != code { 907 t.Errorf("For /%d, status code = %d; want %d", code, res.StatusCode, code) 908 } 909 res.Body.Close() 910 } 911 } 912 913 // verifies that sendfile is being used on Linux 914 func TestLinuxSendfile(t *testing.T) { 915 defer afterTest(t) 916 if runtime.GOOS != "linux" { 917 t.Skip("skipping; linux-only test") 918 } 919 if _, err := exec.LookPath("strace"); err != nil { 920 t.Skip("skipping; strace not found in path") 921 } 922 923 ln, err := net.Listen("tcp", "127.0.0.1:0") 924 if err != nil { 925 t.Fatal(err) 926 } 927 lnf, err := ln.(*net.TCPListener).File() 928 if err != nil { 929 t.Fatal(err) 930 } 931 defer ln.Close() 932 933 syscalls := "sendfile,sendfile64" 934 switch runtime.GOARCH { 935 case "mips64", "mips64le": 936 // mips64 strace doesn't support sendfile64 and will error out 937 // if we specify that with `-e trace='. 938 syscalls = "sendfile" 939 } 940 941 var buf bytes.Buffer 942 child := exec.Command("strace", "-f", "-q", "-e", "trace="+syscalls, os.Args[0], "-test.run=TestLinuxSendfileChild") 943 child.ExtraFiles = append(child.ExtraFiles, lnf) 944 child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...) 945 child.Stdout = &buf 946 child.Stderr = &buf 947 if err := child.Start(); err != nil { 948 t.Skipf("skipping; failed to start straced child: %v", err) 949 } 950 951 res, err := Get(fmt.Sprintf("http://%s/", ln.Addr())) 952 if err != nil { 953 t.Fatalf("http client error: %v", err) 954 } 955 _, err = io.Copy(ioutil.Discard, res.Body) 956 if err != nil { 957 t.Fatalf("client body read error: %v", err) 958 } 959 res.Body.Close() 960 961 // Force child to exit cleanly. 962 Post(fmt.Sprintf("http://%s/quit", ln.Addr()), "", nil) 963 child.Wait() 964 965 rx := regexp.MustCompile(`sendfile(64)?\(\d+,\s*\d+,\s*NULL,\s*\d+\)\s*=\s*\d+\s*\n`) 966 rxResume := regexp.MustCompile(`<\.\.\. sendfile(64)? resumed> \)\s*=\s*\d+\s*\n`) 967 out := buf.String() 968 if !rx.MatchString(out) && !rxResume.MatchString(out) { 969 t.Errorf("no sendfile system call found in:\n%s", out) 970 } 971 } 972 973 func getBody(t *testing.T, testName string, req Request) (*Response, []byte) { 974 r, err := DefaultClient.Do(&req) 975 if err != nil { 976 t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err) 977 } 978 b, err := ioutil.ReadAll(r.Body) 979 if err != nil { 980 t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err) 981 } 982 return r, b 983 } 984 985 // TestLinuxSendfileChild isn't a real test. It's used as a helper process 986 // for TestLinuxSendfile. 987 func TestLinuxSendfileChild(*testing.T) { 988 if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { 989 return 990 } 991 defer os.Exit(0) 992 fd3 := os.NewFile(3, "ephemeral-port-listener") 993 ln, err := net.FileListener(fd3) 994 if err != nil { 995 panic(err) 996 } 997 mux := NewServeMux() 998 mux.Handle("/", FileServer(Dir("testdata"))) 999 mux.HandleFunc("/quit", func(ResponseWriter, *Request) { 1000 os.Exit(0) 1001 }) 1002 s := &Server{Handler: mux} 1003 err = s.Serve(ln) 1004 if err != nil { 1005 panic(err) 1006 } 1007 } 1008 1009 func TestFileServerCleanPath(t *testing.T) { 1010 tests := []struct { 1011 path string 1012 wantCode int 1013 wantOpen []string 1014 }{ 1015 {"/", 200, []string{"/", "/index.html"}}, 1016 {"/dir", 301, []string{"/dir"}}, 1017 {"/dir/", 200, []string{"/dir", "/dir/index.html"}}, 1018 } 1019 for _, tt := range tests { 1020 var log []string 1021 rr := httptest.NewRecorder() 1022 req, _ := NewRequest("GET", "http://foo.localhost"+tt.path, nil) 1023 FileServer(fileServerCleanPathDir{&log}).ServeHTTP(rr, req) 1024 if !reflect.DeepEqual(log, tt.wantOpen) { 1025 t.Logf("For %s: Opens = %q; want %q", tt.path, log, tt.wantOpen) 1026 } 1027 if rr.Code != tt.wantCode { 1028 t.Logf("For %s: Response code = %d; want %d", tt.path, rr.Code, tt.wantCode) 1029 } 1030 } 1031 } 1032 1033 type fileServerCleanPathDir struct { 1034 log *[]string 1035 } 1036 1037 func (d fileServerCleanPathDir) Open(path string) (File, error) { 1038 *(d.log) = append(*(d.log), path) 1039 if path == "/" || path == "/dir" || path == "/dir/" { 1040 // Just return back something that's a directory. 1041 return Dir(".").Open(".") 1042 } 1043 return nil, os.ErrNotExist 1044 } 1045 1046 type panicOnSeek struct{ io.ReadSeeker }