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