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