github.com/ader1990/go@v0.0.0-20140630135419-8c24447fa791/src/pkg/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=20-", code: StatusRequestedRangeNotSatisfiable}, 54 {r: "bytes=0-0,-2", code: StatusPartialContent, ranges: []wantRange{{0, 1}, {testFileLen - 2, testFileLen}}}, 55 {r: "bytes=0-1,5-8", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, 9}}}, 56 {r: "bytes=0-1,5-", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, testFileLen}}}, 57 {r: "bytes=5-1000", code: StatusPartialContent, ranges: []wantRange{{5, testFileLen}}}, 58 {r: "bytes=0-,1-,2-,3-,4-", code: StatusOK}, // ignore wasteful range request 59 {r: "bytes=0-" + itoa(testFileLen-2), code: StatusPartialContent, ranges: []wantRange{{0, testFileLen - 1}}}, 60 {r: "bytes=0-" + itoa(testFileLen-1), code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}}, 61 {r: "bytes=0-" + itoa(testFileLen), code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}}, 62 } 63 64 func TestServeFile(t *testing.T) { 65 defer afterTest(t) 66 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 67 ServeFile(w, r, "testdata/file") 68 })) 69 defer ts.Close() 70 71 var err error 72 73 file, err := ioutil.ReadFile(testFile) 74 if err != nil { 75 t.Fatal("reading file:", err) 76 } 77 78 // set up the Request (re-used for all tests) 79 var req Request 80 req.Header = make(Header) 81 if req.URL, err = url.Parse(ts.URL); err != nil { 82 t.Fatal("ParseURL:", err) 83 } 84 req.Method = "GET" 85 86 // straight GET 87 _, body := getBody(t, "straight get", req) 88 if !bytes.Equal(body, file) { 89 t.Fatalf("body mismatch: got %q, want %q", body, file) 90 } 91 92 // Range tests 93 Cases: 94 for _, rt := range ServeFileRangeTests { 95 if rt.r != "" { 96 req.Header.Set("Range", rt.r) 97 } 98 resp, body := getBody(t, fmt.Sprintf("range test %q", rt.r), req) 99 if resp.StatusCode != rt.code { 100 t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, resp.StatusCode, rt.code) 101 } 102 if rt.code == StatusRequestedRangeNotSatisfiable { 103 continue 104 } 105 wantContentRange := "" 106 if len(rt.ranges) == 1 { 107 rng := rt.ranges[0] 108 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen) 109 } 110 cr := resp.Header.Get("Content-Range") 111 if cr != wantContentRange { 112 t.Errorf("range=%q: Content-Range = %q, want %q", rt.r, cr, wantContentRange) 113 } 114 ct := resp.Header.Get("Content-Type") 115 if len(rt.ranges) == 1 { 116 rng := rt.ranges[0] 117 wantBody := file[rng.start:rng.end] 118 if !bytes.Equal(body, wantBody) { 119 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody) 120 } 121 if strings.HasPrefix(ct, "multipart/byteranges") { 122 t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct) 123 } 124 } 125 if len(rt.ranges) > 1 { 126 typ, params, err := mime.ParseMediaType(ct) 127 if err != nil { 128 t.Errorf("range=%q content-type = %q; %v", rt.r, ct, err) 129 continue 130 } 131 if typ != "multipart/byteranges" { 132 t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ) 133 continue 134 } 135 if params["boundary"] == "" { 136 t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct) 137 continue 138 } 139 if g, w := resp.ContentLength, int64(len(body)); g != w { 140 t.Errorf("range=%q Content-Length = %d; want %d", rt.r, g, w) 141 continue 142 } 143 mr := multipart.NewReader(bytes.NewReader(body), params["boundary"]) 144 for ri, rng := range rt.ranges { 145 part, err := mr.NextPart() 146 if err != nil { 147 t.Errorf("range=%q, reading part index %d: %v", rt.r, ri, err) 148 continue Cases 149 } 150 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen) 151 if g, w := part.Header.Get("Content-Range"), wantContentRange; g != w { 152 t.Errorf("range=%q: part Content-Range = %q; want %q", rt.r, g, w) 153 } 154 body, err := ioutil.ReadAll(part) 155 if err != nil { 156 t.Errorf("range=%q, reading part index %d body: %v", rt.r, ri, err) 157 continue Cases 158 } 159 wantBody := file[rng.start:rng.end] 160 if !bytes.Equal(body, wantBody) { 161 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody) 162 } 163 } 164 _, err = mr.NextPart() 165 if err != io.EOF { 166 t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err) 167 } 168 } 169 } 170 } 171 172 var fsRedirectTestData = []struct { 173 original, redirect string 174 }{ 175 {"/test/index.html", "/test/"}, 176 {"/test/testdata", "/test/testdata/"}, 177 {"/test/testdata/file/", "/test/testdata/file"}, 178 } 179 180 func TestFSRedirect(t *testing.T) { 181 defer afterTest(t) 182 ts := httptest.NewServer(StripPrefix("/test", FileServer(Dir(".")))) 183 defer ts.Close() 184 185 for _, data := range fsRedirectTestData { 186 res, err := Get(ts.URL + data.original) 187 if err != nil { 188 t.Fatal(err) 189 } 190 res.Body.Close() 191 if g, e := res.Request.URL.Path, data.redirect; g != e { 192 t.Errorf("redirect from %s: got %s, want %s", data.original, g, e) 193 } 194 } 195 } 196 197 type testFileSystem struct { 198 open func(name string) (File, error) 199 } 200 201 func (fs *testFileSystem) Open(name string) (File, error) { 202 return fs.open(name) 203 } 204 205 func TestFileServerCleans(t *testing.T) { 206 defer afterTest(t) 207 ch := make(chan string, 1) 208 fs := FileServer(&testFileSystem{func(name string) (File, error) { 209 ch <- name 210 return nil, errors.New("file does not exist") 211 }}) 212 tests := []struct { 213 reqPath, openArg string 214 }{ 215 {"/foo.txt", "/foo.txt"}, 216 {"//foo.txt", "/foo.txt"}, 217 {"/../foo.txt", "/foo.txt"}, 218 } 219 req, _ := NewRequest("GET", "http://example.com", nil) 220 for n, test := range tests { 221 rec := httptest.NewRecorder() 222 req.URL.Path = test.reqPath 223 fs.ServeHTTP(rec, req) 224 if got := <-ch; got != test.openArg { 225 t.Errorf("test %d: got %q, want %q", n, got, test.openArg) 226 } 227 } 228 } 229 230 func TestFileServerEscapesNames(t *testing.T) { 231 defer afterTest(t) 232 const dirListPrefix = "<pre>\n" 233 const dirListSuffix = "\n</pre>\n" 234 tests := []struct { 235 name, escaped string 236 }{ 237 {`simple_name`, `<a href="simple_name">simple_name</a>`}, 238 {`"'<>&`, `<a href="%22%27%3C%3E&">"'<>&</a>`}, 239 {`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`}, 240 {`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo"><combo>?foo</a>`}, 241 } 242 243 // We put each test file in its own directory in the fakeFS so we can look at it in isolation. 244 fs := make(fakeFS) 245 for i, test := range tests { 246 testFile := &fakeFileInfo{basename: test.name} 247 fs[fmt.Sprintf("/%d", i)] = &fakeFileInfo{ 248 dir: true, 249 modtime: time.Unix(1000000000, 0).UTC(), 250 ents: []*fakeFileInfo{testFile}, 251 } 252 fs[fmt.Sprintf("/%d/%s", i, test.name)] = testFile 253 } 254 255 ts := httptest.NewServer(FileServer(&fs)) 256 defer ts.Close() 257 for i, test := range tests { 258 url := fmt.Sprintf("%s/%d", ts.URL, i) 259 res, err := Get(url) 260 if err != nil { 261 t.Fatalf("test %q: Get: %v", test.name, err) 262 } 263 b, err := ioutil.ReadAll(res.Body) 264 if err != nil { 265 t.Fatalf("test %q: read Body: %v", test.name, err) 266 } 267 s := string(b) 268 if !strings.HasPrefix(s, dirListPrefix) || !strings.HasSuffix(s, dirListSuffix) { 269 t.Errorf("test %q: listing dir, full output is %q, want prefix %q and suffix %q", test.name, s, dirListPrefix, dirListSuffix) 270 } 271 if trimmed := strings.TrimSuffix(strings.TrimPrefix(s, dirListPrefix), dirListSuffix); trimmed != test.escaped { 272 t.Errorf("test %q: listing dir, filename escaped to %q, want %q", test.name, trimmed, test.escaped) 273 } 274 res.Body.Close() 275 } 276 } 277 278 func mustRemoveAll(dir string) { 279 err := os.RemoveAll(dir) 280 if err != nil { 281 panic(err) 282 } 283 } 284 285 func TestFileServerImplicitLeadingSlash(t *testing.T) { 286 defer afterTest(t) 287 tempDir, err := ioutil.TempDir("", "") 288 if err != nil { 289 t.Fatalf("TempDir: %v", err) 290 } 291 defer mustRemoveAll(tempDir) 292 if err := ioutil.WriteFile(filepath.Join(tempDir, "foo.txt"), []byte("Hello world"), 0644); err != nil { 293 t.Fatalf("WriteFile: %v", err) 294 } 295 ts := httptest.NewServer(StripPrefix("/bar/", FileServer(Dir(tempDir)))) 296 defer ts.Close() 297 get := func(suffix string) string { 298 res, err := Get(ts.URL + suffix) 299 if err != nil { 300 t.Fatalf("Get %s: %v", suffix, err) 301 } 302 b, err := ioutil.ReadAll(res.Body) 303 if err != nil { 304 t.Fatalf("ReadAll %s: %v", suffix, err) 305 } 306 res.Body.Close() 307 return string(b) 308 } 309 if s := get("/bar/"); !strings.Contains(s, ">foo.txt<") { 310 t.Logf("expected a directory listing with foo.txt, got %q", s) 311 } 312 if s := get("/bar/foo.txt"); s != "Hello world" { 313 t.Logf("expected %q, got %q", "Hello world", s) 314 } 315 } 316 317 func TestDirJoin(t *testing.T) { 318 if runtime.GOOS == "windows" { 319 t.Skip("skipping test on windows") 320 } 321 wfi, err := os.Stat("/etc/hosts") 322 if err != nil { 323 t.Skip("skipping test; no /etc/hosts file") 324 } 325 test := func(d Dir, name string) { 326 f, err := d.Open(name) 327 if err != nil { 328 t.Fatalf("open of %s: %v", name, err) 329 } 330 defer f.Close() 331 gfi, err := f.Stat() 332 if err != nil { 333 t.Fatalf("stat of %s: %v", name, err) 334 } 335 if !os.SameFile(gfi, wfi) { 336 t.Errorf("%s got different file", name) 337 } 338 } 339 test(Dir("/etc/"), "/hosts") 340 test(Dir("/etc/"), "hosts") 341 test(Dir("/etc/"), "../../../../hosts") 342 test(Dir("/etc"), "/hosts") 343 test(Dir("/etc"), "hosts") 344 test(Dir("/etc"), "../../../../hosts") 345 346 // Not really directories, but since we use this trick in 347 // ServeFile, test it: 348 test(Dir("/etc/hosts"), "") 349 test(Dir("/etc/hosts"), "/") 350 test(Dir("/etc/hosts"), "../") 351 } 352 353 func TestEmptyDirOpenCWD(t *testing.T) { 354 test := func(d Dir) { 355 name := "fs_test.go" 356 f, err := d.Open(name) 357 if err != nil { 358 t.Fatalf("open of %s: %v", name, err) 359 } 360 defer f.Close() 361 } 362 test(Dir("")) 363 test(Dir(".")) 364 test(Dir("./")) 365 } 366 367 func TestServeFileContentType(t *testing.T) { 368 defer afterTest(t) 369 const ctype = "icecream/chocolate" 370 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 371 switch r.FormValue("override") { 372 case "1": 373 w.Header().Set("Content-Type", ctype) 374 case "2": 375 // Explicitly inhibit sniffing. 376 w.Header()["Content-Type"] = []string{} 377 } 378 ServeFile(w, r, "testdata/file") 379 })) 380 defer ts.Close() 381 get := func(override string, want []string) { 382 resp, err := Get(ts.URL + "?override=" + override) 383 if err != nil { 384 t.Fatal(err) 385 } 386 if h := resp.Header["Content-Type"]; !reflect.DeepEqual(h, want) { 387 t.Errorf("Content-Type mismatch: got %v, want %v", h, want) 388 } 389 resp.Body.Close() 390 } 391 get("0", []string{"text/plain; charset=utf-8"}) 392 get("1", []string{ctype}) 393 get("2", nil) 394 } 395 396 func TestServeFileMimeType(t *testing.T) { 397 defer afterTest(t) 398 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 399 ServeFile(w, r, "testdata/style.css") 400 })) 401 defer ts.Close() 402 resp, err := Get(ts.URL) 403 if err != nil { 404 t.Fatal(err) 405 } 406 resp.Body.Close() 407 want := "text/css; charset=utf-8" 408 if h := resp.Header.Get("Content-Type"); h != want { 409 t.Errorf("Content-Type mismatch: got %q, want %q", h, want) 410 } 411 } 412 413 func TestServeFileFromCWD(t *testing.T) { 414 defer afterTest(t) 415 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 416 ServeFile(w, r, "fs_test.go") 417 })) 418 defer ts.Close() 419 r, err := Get(ts.URL) 420 if err != nil { 421 t.Fatal(err) 422 } 423 r.Body.Close() 424 if r.StatusCode != 200 { 425 t.Fatalf("expected 200 OK, got %s", r.Status) 426 } 427 } 428 429 func TestServeFileWithContentEncoding(t *testing.T) { 430 defer afterTest(t) 431 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 432 w.Header().Set("Content-Encoding", "foo") 433 ServeFile(w, r, "testdata/file") 434 })) 435 defer ts.Close() 436 resp, err := Get(ts.URL) 437 if err != nil { 438 t.Fatal(err) 439 } 440 resp.Body.Close() 441 if g, e := resp.ContentLength, int64(-1); g != e { 442 t.Errorf("Content-Length mismatch: got %d, want %d", g, e) 443 } 444 } 445 446 func TestServeIndexHtml(t *testing.T) { 447 defer afterTest(t) 448 const want = "index.html says hello\n" 449 ts := httptest.NewServer(FileServer(Dir("."))) 450 defer ts.Close() 451 452 for _, path := range []string{"/testdata/", "/testdata/index.html"} { 453 res, err := Get(ts.URL + path) 454 if err != nil { 455 t.Fatal(err) 456 } 457 b, err := ioutil.ReadAll(res.Body) 458 if err != nil { 459 t.Fatal("reading Body:", err) 460 } 461 if s := string(b); s != want { 462 t.Errorf("for path %q got %q, want %q", path, s, want) 463 } 464 res.Body.Close() 465 } 466 } 467 468 func TestFileServerZeroByte(t *testing.T) { 469 defer afterTest(t) 470 ts := httptest.NewServer(FileServer(Dir("."))) 471 defer ts.Close() 472 473 res, err := Get(ts.URL + "/..\x00") 474 if err != nil { 475 t.Fatal(err) 476 } 477 b, err := ioutil.ReadAll(res.Body) 478 if err != nil { 479 t.Fatal("reading Body:", err) 480 } 481 if res.StatusCode == 200 { 482 t.Errorf("got status 200; want an error. Body is:\n%s", string(b)) 483 } 484 } 485 486 type fakeFileInfo struct { 487 dir bool 488 basename string 489 modtime time.Time 490 ents []*fakeFileInfo 491 contents string 492 } 493 494 func (f *fakeFileInfo) Name() string { return f.basename } 495 func (f *fakeFileInfo) Sys() interface{} { return nil } 496 func (f *fakeFileInfo) ModTime() time.Time { return f.modtime } 497 func (f *fakeFileInfo) IsDir() bool { return f.dir } 498 func (f *fakeFileInfo) Size() int64 { return int64(len(f.contents)) } 499 func (f *fakeFileInfo) Mode() os.FileMode { 500 if f.dir { 501 return 0755 | os.ModeDir 502 } 503 return 0644 504 } 505 506 type fakeFile struct { 507 io.ReadSeeker 508 fi *fakeFileInfo 509 path string // as opened 510 entpos int 511 } 512 513 func (f *fakeFile) Close() error { return nil } 514 func (f *fakeFile) Stat() (os.FileInfo, error) { return f.fi, nil } 515 func (f *fakeFile) Readdir(count int) ([]os.FileInfo, error) { 516 if !f.fi.dir { 517 return nil, os.ErrInvalid 518 } 519 var fis []os.FileInfo 520 521 limit := f.entpos + count 522 if count <= 0 || limit > len(f.fi.ents) { 523 limit = len(f.fi.ents) 524 } 525 for ; f.entpos < limit; f.entpos++ { 526 fis = append(fis, f.fi.ents[f.entpos]) 527 } 528 529 if len(fis) == 0 && count > 0 { 530 return fis, io.EOF 531 } else { 532 return fis, nil 533 } 534 } 535 536 type fakeFS map[string]*fakeFileInfo 537 538 func (fs fakeFS) Open(name string) (File, error) { 539 name = path.Clean(name) 540 f, ok := fs[name] 541 if !ok { 542 return nil, os.ErrNotExist 543 } 544 return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil 545 } 546 547 func TestDirectoryIfNotModified(t *testing.T) { 548 defer afterTest(t) 549 const indexContents = "I am a fake index.html file" 550 fileMod := time.Unix(1000000000, 0).UTC() 551 fileModStr := fileMod.Format(TimeFormat) 552 dirMod := time.Unix(123, 0).UTC() 553 indexFile := &fakeFileInfo{ 554 basename: "index.html", 555 modtime: fileMod, 556 contents: indexContents, 557 } 558 fs := fakeFS{ 559 "/": &fakeFileInfo{ 560 dir: true, 561 modtime: dirMod, 562 ents: []*fakeFileInfo{indexFile}, 563 }, 564 "/index.html": indexFile, 565 } 566 567 ts := httptest.NewServer(FileServer(fs)) 568 defer ts.Close() 569 570 res, err := Get(ts.URL) 571 if err != nil { 572 t.Fatal(err) 573 } 574 b, err := ioutil.ReadAll(res.Body) 575 if err != nil { 576 t.Fatal(err) 577 } 578 if string(b) != indexContents { 579 t.Fatalf("Got body %q; want %q", b, indexContents) 580 } 581 res.Body.Close() 582 583 lastMod := res.Header.Get("Last-Modified") 584 if lastMod != fileModStr { 585 t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr) 586 } 587 588 req, _ := NewRequest("GET", ts.URL, nil) 589 req.Header.Set("If-Modified-Since", lastMod) 590 591 res, err = DefaultClient.Do(req) 592 if err != nil { 593 t.Fatal(err) 594 } 595 if res.StatusCode != 304 { 596 t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode) 597 } 598 res.Body.Close() 599 600 // Advance the index.html file's modtime, but not the directory's. 601 indexFile.modtime = indexFile.modtime.Add(1 * time.Hour) 602 603 res, err = DefaultClient.Do(req) 604 if err != nil { 605 t.Fatal(err) 606 } 607 if res.StatusCode != 200 { 608 t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res) 609 } 610 res.Body.Close() 611 } 612 613 func mustStat(t *testing.T, fileName string) os.FileInfo { 614 fi, err := os.Stat(fileName) 615 if err != nil { 616 t.Fatal(err) 617 } 618 return fi 619 } 620 621 func TestServeContent(t *testing.T) { 622 defer afterTest(t) 623 type serveParam struct { 624 name string 625 modtime time.Time 626 content io.ReadSeeker 627 contentType string 628 etag string 629 } 630 servec := make(chan serveParam, 1) 631 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { 632 p := <-servec 633 if p.etag != "" { 634 w.Header().Set("ETag", p.etag) 635 } 636 if p.contentType != "" { 637 w.Header().Set("Content-Type", p.contentType) 638 } 639 ServeContent(w, r, p.name, p.modtime, p.content) 640 })) 641 defer ts.Close() 642 643 type testCase struct { 644 // One of file or content must be set: 645 file string 646 content io.ReadSeeker 647 648 modtime time.Time 649 serveETag string // optional 650 serveContentType string // optional 651 reqHeader map[string]string 652 wantLastMod string 653 wantContentType string 654 wantStatus int 655 } 656 htmlModTime := mustStat(t, "testdata/index.html").ModTime() 657 tests := map[string]testCase{ 658 "no_last_modified": { 659 file: "testdata/style.css", 660 wantContentType: "text/css; charset=utf-8", 661 wantStatus: 200, 662 }, 663 "with_last_modified": { 664 file: "testdata/index.html", 665 wantContentType: "text/html; charset=utf-8", 666 modtime: htmlModTime, 667 wantLastMod: htmlModTime.UTC().Format(TimeFormat), 668 wantStatus: 200, 669 }, 670 "not_modified_modtime": { 671 file: "testdata/style.css", 672 modtime: htmlModTime, 673 reqHeader: map[string]string{ 674 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat), 675 }, 676 wantStatus: 304, 677 }, 678 "not_modified_modtime_with_contenttype": { 679 file: "testdata/style.css", 680 serveContentType: "text/css", // explicit content type 681 modtime: htmlModTime, 682 reqHeader: map[string]string{ 683 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat), 684 }, 685 wantStatus: 304, 686 }, 687 "not_modified_etag": { 688 file: "testdata/style.css", 689 serveETag: `"foo"`, 690 reqHeader: map[string]string{ 691 "If-None-Match": `"foo"`, 692 }, 693 wantStatus: 304, 694 }, 695 "not_modified_etag_no_seek": { 696 content: panicOnSeek{nil}, // should never be called 697 serveETag: `"foo"`, 698 reqHeader: map[string]string{ 699 "If-None-Match": `"foo"`, 700 }, 701 wantStatus: 304, 702 }, 703 "range_good": { 704 file: "testdata/style.css", 705 serveETag: `"A"`, 706 reqHeader: map[string]string{ 707 "Range": "bytes=0-4", 708 }, 709 wantStatus: StatusPartialContent, 710 wantContentType: "text/css; charset=utf-8", 711 }, 712 // An If-Range resource for entity "A", but entity "B" is now current. 713 // The Range request should be ignored. 714 "range_no_match": { 715 file: "testdata/style.css", 716 serveETag: `"A"`, 717 reqHeader: map[string]string{ 718 "Range": "bytes=0-4", 719 "If-Range": `"B"`, 720 }, 721 wantStatus: 200, 722 wantContentType: "text/css; charset=utf-8", 723 }, 724 } 725 for testName, tt := range tests { 726 var content io.ReadSeeker 727 if tt.file != "" { 728 f, err := os.Open(tt.file) 729 if err != nil { 730 t.Fatalf("test %q: %v", testName, err) 731 } 732 defer f.Close() 733 content = f 734 } else { 735 content = tt.content 736 } 737 738 servec <- serveParam{ 739 name: filepath.Base(tt.file), 740 content: content, 741 modtime: tt.modtime, 742 etag: tt.serveETag, 743 contentType: tt.serveContentType, 744 } 745 req, err := NewRequest("GET", ts.URL, nil) 746 if err != nil { 747 t.Fatal(err) 748 } 749 for k, v := range tt.reqHeader { 750 req.Header.Set(k, v) 751 } 752 res, err := DefaultClient.Do(req) 753 if err != nil { 754 t.Fatal(err) 755 } 756 io.Copy(ioutil.Discard, res.Body) 757 res.Body.Close() 758 if res.StatusCode != tt.wantStatus { 759 t.Errorf("test %q: status = %d; want %d", testName, res.StatusCode, tt.wantStatus) 760 } 761 if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e { 762 t.Errorf("test %q: content-type = %q, want %q", testName, g, e) 763 } 764 if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e { 765 t.Errorf("test %q: last-modified = %q, want %q", testName, g, e) 766 } 767 } 768 } 769 770 // verifies that sendfile is being used on Linux 771 func TestLinuxSendfile(t *testing.T) { 772 defer afterTest(t) 773 if runtime.GOOS != "linux" { 774 t.Skip("skipping; linux-only test") 775 } 776 if _, err := exec.LookPath("strace"); err != nil { 777 t.Skip("skipping; strace not found in path") 778 } 779 780 ln, err := net.Listen("tcp", "127.0.0.1:0") 781 if err != nil { 782 t.Fatal(err) 783 } 784 lnf, err := ln.(*net.TCPListener).File() 785 if err != nil { 786 t.Fatal(err) 787 } 788 defer ln.Close() 789 790 var buf bytes.Buffer 791 child := exec.Command("strace", "-f", "-q", "-e", "trace=sendfile,sendfile64", os.Args[0], "-test.run=TestLinuxSendfileChild") 792 child.ExtraFiles = append(child.ExtraFiles, lnf) 793 child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...) 794 child.Stdout = &buf 795 child.Stderr = &buf 796 if err := child.Start(); err != nil { 797 t.Skipf("skipping; failed to start straced child: %v", err) 798 } 799 800 res, err := Get(fmt.Sprintf("http://%s/", ln.Addr())) 801 if err != nil { 802 t.Fatalf("http client error: %v", err) 803 } 804 _, err = io.Copy(ioutil.Discard, res.Body) 805 if err != nil { 806 t.Fatalf("client body read error: %v", err) 807 } 808 res.Body.Close() 809 810 // Force child to exit cleanly. 811 Get(fmt.Sprintf("http://%s/quit", ln.Addr())) 812 child.Wait() 813 814 rx := regexp.MustCompile(`sendfile(64)?\(\d+,\s*\d+,\s*NULL,\s*\d+\)\s*=\s*\d+\s*\n`) 815 rxResume := regexp.MustCompile(`<\.\.\. sendfile(64)? resumed> \)\s*=\s*\d+\s*\n`) 816 out := buf.String() 817 if !rx.MatchString(out) && !rxResume.MatchString(out) { 818 t.Errorf("no sendfile system call found in:\n%s", out) 819 } 820 } 821 822 func getBody(t *testing.T, testName string, req Request) (*Response, []byte) { 823 r, err := DefaultClient.Do(&req) 824 if err != nil { 825 t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err) 826 } 827 b, err := ioutil.ReadAll(r.Body) 828 if err != nil { 829 t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err) 830 } 831 return r, b 832 } 833 834 // TestLinuxSendfileChild isn't a real test. It's used as a helper process 835 // for TestLinuxSendfile. 836 func TestLinuxSendfileChild(*testing.T) { 837 if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { 838 return 839 } 840 defer os.Exit(0) 841 fd3 := os.NewFile(3, "ephemeral-port-listener") 842 ln, err := net.FileListener(fd3) 843 if err != nil { 844 panic(err) 845 } 846 mux := NewServeMux() 847 mux.Handle("/", FileServer(Dir("testdata"))) 848 mux.HandleFunc("/quit", func(ResponseWriter, *Request) { 849 os.Exit(0) 850 }) 851 s := &Server{Handler: mux} 852 err = s.Serve(ln) 853 if err != nil { 854 panic(err) 855 } 856 } 857 858 type panicOnSeek struct{ io.ReadSeeker }