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.
31 testFile
= "testdata/file"
35 type wantRange
struct {
36 start
, end
int64 // range [start,end)
39 var ServeFileRangeTests
= []struct {
44 {r
: "", code
: StatusOK
},
45 {r
: "bytes=0-4", code
: StatusPartialContent
, ranges
: []wantRange
{{0, 5}}},
46 {r
: "bytes=2-", code
: StatusPartialContent
, ranges
: []wantRange
{{2, testFileLen
}}},
47 {r
: "bytes=-5", code
: StatusPartialContent
, ranges
: []wantRange
{{testFileLen
- 5, testFileLen
}}},
48 {r
: "bytes=3-7", code
: StatusPartialContent
, ranges
: []wantRange
{{3, 8}}},
49 {r
: "bytes=20-", code
: StatusRequestedRangeNotSatisfiable
},
50 {r
: "bytes=0-0,-2", code
: StatusPartialContent
, ranges
: []wantRange
{{0, 1}, {testFileLen
- 2, testFileLen
}}},
51 {r
: "bytes=0-1,5-8", code
: StatusPartialContent
, ranges
: []wantRange
{{0, 2}, {5, 9}}},
52 {r
: "bytes=0-1,5-", code
: StatusPartialContent
, ranges
: []wantRange
{{0, 2}, {5, testFileLen
}}},
53 {r
: "bytes=0-,1-,2-,3-,4-", code
: StatusOK
}, // ignore wasteful range request
56 func TestServeFile(t
*testing
.T
) {
58 ts
:= httptest
.NewServer(HandlerFunc(func(w ResponseWriter
, r
*Request
) {
59 ServeFile(w
, r
, "testdata/file")
65 file
, err
:= ioutil
.ReadFile(testFile
)
67 t
.Fatal("reading file:", err
)
70 // set up the Request (re-used for all tests)
72 req
.Header
= make(Header
)
73 if req
.URL
, err
= url
.Parse(ts
.URL
); err
!= nil {
74 t
.Fatal("ParseURL:", err
)
79 _
, body
:= getBody(t
, "straight get", req
)
80 if !bytes
.Equal(body
, file
) {
81 t
.Fatalf("body mismatch: got %q, want %q", body
, file
)
86 for _
, rt
:= range ServeFileRangeTests
{
88 req
.Header
.Set("Range", rt
.r
)
90 resp
, body
:= getBody(t
, fmt
.Sprintf("range test %q", rt
.r
), req
)
91 if resp
.StatusCode
!= rt
.code
{
92 t
.Errorf("range=%q: StatusCode=%d, want %d", rt
.r
, resp
.StatusCode
, rt
.code
)
94 if rt
.code
== StatusRequestedRangeNotSatisfiable
{
97 wantContentRange
:= ""
98 if len(rt
.ranges
) == 1 {
100 wantContentRange
= fmt
.Sprintf("bytes %d-%d/%d", rng
.start
, rng
.end
-1, testFileLen
)
102 cr
:= resp
.Header
.Get("Content-Range")
103 if cr
!= wantContentRange
{
104 t
.Errorf("range=%q: Content-Range = %q, want %q", rt
.r
, cr
, wantContentRange
)
106 ct
:= resp
.Header
.Get("Content-Type")
107 if len(rt
.ranges
) == 1 {
109 wantBody
:= file
[rng
.start
:rng
.end
]
110 if !bytes
.Equal(body
, wantBody
) {
111 t
.Errorf("range=%q: body = %q, want %q", rt
.r
, body
, wantBody
)
113 if strings
.HasPrefix(ct
, "multipart/byteranges") {
114 t
.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt
.r
, ct
)
117 if len(rt
.ranges
) > 1 {
118 typ
, params
, err
:= mime
.ParseMediaType(ct
)
120 t
.Errorf("range=%q content-type = %q; %v", rt
.r
, ct
, err
)
123 if typ
!= "multipart/byteranges" {
124 t
.Errorf("range=%q content-type = %q; want multipart/byteranges", rt
.r
, typ
)
127 if params
["boundary"] == "" {
128 t
.Errorf("range=%q content-type = %q; lacks boundary", rt
.r
, ct
)
131 if g
, w
:= resp
.ContentLength
, int64(len(body
)); g
!= w
{
132 t
.Errorf("range=%q Content-Length = %d; want %d", rt
.r
, g
, w
)
135 mr
:= multipart
.NewReader(bytes
.NewReader(body
), params
["boundary"])
136 for ri
, rng
:= range rt
.ranges
{
137 part
, err
:= mr
.NextPart()
139 t
.Errorf("range=%q, reading part index %d: %v", rt
.r
, ri
, err
)
142 wantContentRange
= fmt
.Sprintf("bytes %d-%d/%d", rng
.start
, rng
.end
-1, testFileLen
)
143 if g
, w
:= part
.Header
.Get("Content-Range"), wantContentRange
; g
!= w
{
144 t
.Errorf("range=%q: part Content-Range = %q; want %q", rt
.r
, g
, w
)
146 body
, err
:= ioutil
.ReadAll(part
)
148 t
.Errorf("range=%q, reading part index %d body: %v", rt
.r
, ri
, err
)
151 wantBody
:= file
[rng
.start
:rng
.end
]
152 if !bytes
.Equal(body
, wantBody
) {
153 t
.Errorf("range=%q: body = %q, want %q", rt
.r
, body
, wantBody
)
156 _
, err
= mr
.NextPart()
158 t
.Errorf("range=%q; expected final error io.EOF; got %v", rt
.r
, err
)
164 var fsRedirectTestData
= []struct {
165 original
, redirect
string
167 {"/test/index.html", "/test/"},
168 {"/test/testdata", "/test/testdata/"},
169 {"/test/testdata/file/", "/test/testdata/file"},
172 func TestFSRedirect(t
*testing
.T
) {
174 ts
:= httptest
.NewServer(StripPrefix("/test", FileServer(Dir("."))))
177 for _
, data
:= range fsRedirectTestData
{
178 res
, err
:= Get(ts
.URL
+ data
.original
)
183 if g
, e
:= res
.Request
.URL
.Path
, data
.redirect
; g
!= e
{
184 t
.Errorf("redirect from %s: got %s, want %s", data
.original
, g
, e
)
189 type testFileSystem
struct {
190 open
func(name
string) (File
, error
)
193 func (fs
*testFileSystem
) Open(name
string) (File
, error
) {
197 func TestFileServerCleans(t
*testing
.T
) {
199 ch
:= make(chan string, 1)
200 fs
:= FileServer(&testFileSystem
{func(name
string) (File
, error
) {
202 return nil, errors
.New("file does not exist")
205 reqPath
, openArg
string
207 {"/foo.txt", "/foo.txt"},
208 {"//foo.txt", "/foo.txt"},
209 {"/../foo.txt", "/foo.txt"},
211 req
, _
:= NewRequest("GET", "http://example.com", nil)
212 for n
, test
:= range tests
{
213 rec
:= httptest
.NewRecorder()
214 req
.URL
.Path
= test
.reqPath
215 fs
.ServeHTTP(rec
, req
)
216 if got
:= <-ch
; got
!= test
.openArg
{
217 t
.Errorf("test %d: got %q, want %q", n
, got
, test
.openArg
)
222 func mustRemoveAll(dir
string) {
223 err
:= os
.RemoveAll(dir
)
229 func TestFileServerImplicitLeadingSlash(t
*testing
.T
) {
231 tempDir
, err
:= ioutil
.TempDir("", "")
233 t
.Fatalf("TempDir: %v", err
)
235 defer mustRemoveAll(tempDir
)
236 if err
:= ioutil
.WriteFile(filepath
.Join(tempDir
, "foo.txt"), []byte("Hello world"), 0644); err
!= nil {
237 t
.Fatalf("WriteFile: %v", err
)
239 ts
:= httptest
.NewServer(StripPrefix("/bar/", FileServer(Dir(tempDir
))))
241 get
:= func(suffix
string) string {
242 res
, err
:= Get(ts
.URL
+ suffix
)
244 t
.Fatalf("Get %s: %v", suffix
, err
)
246 b
, err
:= ioutil
.ReadAll(res
.Body
)
248 t
.Fatalf("ReadAll %s: %v", suffix
, err
)
253 if s
:= get("/bar/"); !strings
.Contains(s
, ">foo.txt<") {
254 t
.Logf("expected a directory listing with foo.txt, got %q", s
)
256 if s
:= get("/bar/foo.txt"); s
!= "Hello world" {
257 t
.Logf("expected %q, got %q", "Hello world", s
)
261 func TestDirJoin(t
*testing
.T
) {
262 wfi
, err
:= os
.Stat("/etc/hosts")
264 t
.Skip("skipping test; no /etc/hosts file")
266 test
:= func(d Dir
, name
string) {
267 f
, err
:= d
.Open(name
)
269 t
.Fatalf("open of %s: %v", name
, err
)
274 t
.Fatalf("stat of %s: %v", name
, err
)
276 if !os
.SameFile(gfi
, wfi
) {
277 t
.Errorf("%s got different file", name
)
280 test(Dir("/etc/"), "/hosts")
281 test(Dir("/etc/"), "hosts")
282 test(Dir("/etc/"), "../../../../hosts")
283 test(Dir("/etc"), "/hosts")
284 test(Dir("/etc"), "hosts")
285 test(Dir("/etc"), "../../../../hosts")
287 // Not really directories, but since we use this trick in
288 // ServeFile, test it:
289 test(Dir("/etc/hosts"), "")
290 test(Dir("/etc/hosts"), "/")
291 test(Dir("/etc/hosts"), "../")
294 func TestEmptyDirOpenCWD(t
*testing
.T
) {
295 test
:= func(d Dir
) {
297 f
, err
:= d
.Open(name
)
299 t
.Fatalf("open of %s: %v", name
, err
)
308 func TestServeFileContentType(t
*testing
.T
) {
310 const ctype
= "icecream/chocolate"
311 ts
:= httptest
.NewServer(HandlerFunc(func(w ResponseWriter
, r
*Request
) {
312 if r
.FormValue("override") == "1" {
313 w
.Header().Set("Content-Type", ctype
)
315 ServeFile(w
, r
, "testdata/file")
318 get
:= func(override
, want
string) {
319 resp
, err
:= Get(ts
.URL
+ "?override=" + override
)
323 if h
:= resp
.Header
.Get("Content-Type"); h
!= want
{
324 t
.Errorf("Content-Type mismatch: got %q, want %q", h
, want
)
328 get("0", "text/plain; charset=utf-8")
332 func TestServeFileMimeType(t
*testing
.T
) {
334 ts
:= httptest
.NewServer(HandlerFunc(func(w ResponseWriter
, r
*Request
) {
335 ServeFile(w
, r
, "testdata/style.css")
338 resp
, err
:= Get(ts
.URL
)
343 want
:= "text/css; charset=utf-8"
344 if h
:= resp
.Header
.Get("Content-Type"); h
!= want
{
345 t
.Errorf("Content-Type mismatch: got %q, want %q", h
, want
)
349 func TestServeFileFromCWD(t
*testing
.T
) {
351 ts
:= httptest
.NewServer(HandlerFunc(func(w ResponseWriter
, r
*Request
) {
352 ServeFile(w
, r
, "fs_test.go")
355 r
, err
:= Get(ts
.URL
)
360 if r
.StatusCode
!= 200 {
361 t
.Fatalf("expected 200 OK, got %s", r
.Status
)
365 func TestServeFileWithContentEncoding(t
*testing
.T
) {
367 ts
:= httptest
.NewServer(HandlerFunc(func(w ResponseWriter
, r
*Request
) {
368 w
.Header().Set("Content-Encoding", "foo")
369 ServeFile(w
, r
, "testdata/file")
372 resp
, err
:= Get(ts
.URL
)
377 if g
, e
:= resp
.ContentLength
, int64(-1); g
!= e
{
378 t
.Errorf("Content-Length mismatch: got %d, want %d", g
, e
)
382 func TestServeIndexHtml(t
*testing
.T
) {
384 const want
= "index.html says hello\n"
385 ts
:= httptest
.NewServer(FileServer(Dir(".")))
388 for _
, path
:= range []string{"/testdata/", "/testdata/index.html"} {
389 res
, err
:= Get(ts
.URL
+ path
)
393 b
, err
:= ioutil
.ReadAll(res
.Body
)
395 t
.Fatal("reading Body:", err
)
397 if s
:= string(b
); s
!= want
{
398 t
.Errorf("for path %q got %q, want %q", path
, s
, want
)
404 func TestFileServerZeroByte(t
*testing
.T
) {
406 ts
:= httptest
.NewServer(FileServer(Dir(".")))
409 res
, err
:= Get(ts
.URL
+ "/..\x00")
413 b
, err
:= ioutil
.ReadAll(res
.Body
)
415 t
.Fatal("reading Body:", err
)
417 if res
.StatusCode
== 200 {
418 t
.Errorf("got status 200; want an error. Body is:\n%s", string(b
))
422 type fakeFileInfo
struct {
430 func (f
*fakeFileInfo
) Name() string { return f
.basename
}
431 func (f
*fakeFileInfo
) Sys() interface{} { return nil }
432 func (f
*fakeFileInfo
) ModTime() time
.Time
{ return f
.modtime
}
433 func (f
*fakeFileInfo
) IsDir() bool { return f
.dir
}
434 func (f
*fakeFileInfo
) Size() int64 { return int64(len(f
.contents
)) }
435 func (f
*fakeFileInfo
) Mode() os
.FileMode
{
437 return 0755 | os
.ModeDir
442 type fakeFile
struct {
445 path
string // as opened
448 func (f
*fakeFile
) Close() error
{ return nil }
449 func (f
*fakeFile
) Stat() (os
.FileInfo
, error
) { return f
.fi
, nil }
450 func (f
*fakeFile
) Readdir(count
int) ([]os
.FileInfo
, error
) {
452 return nil, os
.ErrInvalid
454 var fis
[]os
.FileInfo
455 for _
, fi
:= range f
.fi
.ents
{
456 fis
= append(fis
, fi
)
461 type fakeFS
map[string]*fakeFileInfo
463 func (fs fakeFS
) Open(name
string) (File
, error
) {
464 name
= path
.Clean(name
)
467 println("fake filesystem didn't find file", name
)
468 return nil, os
.ErrNotExist
470 return &fakeFile
{ReadSeeker
: strings
.NewReader(f
.contents
), fi
: f
, path
: name
}, nil
473 func TestDirectoryIfNotModified(t
*testing
.T
) {
475 const indexContents
= "I am a fake index.html file"
476 fileMod
:= time
.Unix(1000000000, 0).UTC()
477 fileModStr
:= fileMod
.Format(TimeFormat
)
478 dirMod
:= time
.Unix(123, 0).UTC()
479 indexFile
:= &fakeFileInfo
{
480 basename
: "index.html",
482 contents
: indexContents
,
488 ents
: []*fakeFileInfo
{indexFile
},
490 "/index.html": indexFile
,
493 ts
:= httptest
.NewServer(FileServer(fs
))
496 res
, err
:= Get(ts
.URL
)
500 b
, err
:= ioutil
.ReadAll(res
.Body
)
504 if string(b
) != indexContents
{
505 t
.Fatalf("Got body %q; want %q", b
, indexContents
)
509 lastMod
:= res
.Header
.Get("Last-Modified")
510 if lastMod
!= fileModStr
{
511 t
.Fatalf("initial Last-Modified = %q; want %q", lastMod
, fileModStr
)
514 req
, _
:= NewRequest("GET", ts
.URL
, nil)
515 req
.Header
.Set("If-Modified-Since", lastMod
)
517 res
, err
= DefaultClient
.Do(req
)
521 if res
.StatusCode
!= 304 {
522 t
.Fatalf("Code after If-Modified-Since request = %v; want 304", res
.StatusCode
)
526 // Advance the index.html file's modtime, but not the directory's.
527 indexFile
.modtime
= indexFile
.modtime
.Add(1 * time
.Hour
)
529 res
, err
= DefaultClient
.Do(req
)
533 if res
.StatusCode
!= 200 {
534 t
.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res
.StatusCode
, res
)
539 func mustStat(t
*testing
.T
, fileName
string) os
.FileInfo
{
540 fi
, err
:= os
.Stat(fileName
)
547 func TestServeContent(t
*testing
.T
) {
549 type serveParam
struct {
552 content io
.ReadSeeker
556 servec
:= make(chan serveParam
, 1)
557 ts
:= httptest
.NewServer(HandlerFunc(func(w ResponseWriter
, r
*Request
) {
560 w
.Header().Set("ETag", p
.etag
)
562 if p
.contentType
!= "" {
563 w
.Header().Set("Content-Type", p
.contentType
)
565 ServeContent(w
, r
, p
.name
, p
.modtime
, p
.content
)
569 type testCase
struct {
572 serveETag
string // optional
573 serveContentType
string // optional
574 reqHeader
map[string]string
576 wantContentType
string
579 htmlModTime
:= mustStat(t
, "testdata/index.html").ModTime()
580 tests
:= map[string]testCase
{
581 "no_last_modified": {
582 file
: "testdata/style.css",
583 wantContentType
: "text/css; charset=utf-8",
586 "with_last_modified": {
587 file
: "testdata/index.html",
588 wantContentType
: "text/html; charset=utf-8",
589 modtime
: htmlModTime
,
590 wantLastMod
: htmlModTime
.UTC().Format(TimeFormat
),
593 "not_modified_modtime": {
594 file
: "testdata/style.css",
595 modtime
: htmlModTime
,
596 reqHeader
: map[string]string{
597 "If-Modified-Since": htmlModTime
.UTC().Format(TimeFormat
),
601 "not_modified_modtime_with_contenttype": {
602 file
: "testdata/style.css",
603 serveContentType
: "text/css", // explicit content type
604 modtime
: htmlModTime
,
605 reqHeader
: map[string]string{
606 "If-Modified-Since": htmlModTime
.UTC().Format(TimeFormat
),
610 "not_modified_etag": {
611 file
: "testdata/style.css",
613 reqHeader
: map[string]string{
614 "If-None-Match": `"foo"`,
619 file
: "testdata/style.css",
621 reqHeader
: map[string]string{
622 "Range": "bytes=0-4",
624 wantStatus
: StatusPartialContent
,
625 wantContentType
: "text/css; charset=utf-8",
627 // An If-Range resource for entity "A", but entity "B" is now current.
628 // The Range request should be ignored.
630 file
: "testdata/style.css",
632 reqHeader
: map[string]string{
633 "Range": "bytes=0-4",
637 wantContentType
: "text/css; charset=utf-8",
640 for testName
, tt
:= range tests
{
641 f
, err
:= os
.Open(tt
.file
)
643 t
.Fatalf("test %q: %v", testName
, err
)
647 servec
<- serveParam
{
648 name
: filepath
.Base(tt
.file
),
652 contentType
: tt
.serveContentType
,
654 req
, err
:= NewRequest("GET", ts
.URL
, nil)
658 for k
, v
:= range tt
.reqHeader
{
661 res
, err
:= DefaultClient
.Do(req
)
665 io
.Copy(ioutil
.Discard
, res
.Body
)
667 if res
.StatusCode
!= tt
.wantStatus
{
668 t
.Errorf("test %q: status = %d; want %d", testName
, res
.StatusCode
, tt
.wantStatus
)
670 if g
, e
:= res
.Header
.Get("Content-Type"), tt
.wantContentType
; g
!= e
{
671 t
.Errorf("test %q: content-type = %q, want %q", testName
, g
, e
)
673 if g
, e
:= res
.Header
.Get("Last-Modified"), tt
.wantLastMod
; g
!= e
{
674 t
.Errorf("test %q: last-modified = %q, want %q", testName
, g
, e
)
679 // verifies that sendfile is being used on Linux
680 func TestLinuxSendfile(t
*testing
.T
) {
682 if runtime
.GOOS
!= "linux" {
683 t
.Skip("skipping; linux-only test")
685 if _
, err
:= exec
.LookPath("strace"); err
!= nil {
686 t
.Skip("skipping; strace not found in path")
689 ln
, err
:= net
.Listen("tcp", "127.0.0.1:0")
693 lnf
, err
:= ln
.(*net
.TCPListener
).File()
700 child
:= exec
.Command("strace", "-f", "-q", "-e", "trace=sendfile,sendfile64", os
.Args
[0], "-test.run=TestLinuxSendfileChild")
701 child
.ExtraFiles
= append(child
.ExtraFiles
, lnf
)
702 child
.Env
= append([]string{"GO_WANT_HELPER_PROCESS=1"}, os
.Environ()...)
705 if err
:= child
.Start(); err
!= nil {
706 t
.Skipf("skipping; failed to start straced child: %v", err
)
709 res
, err
:= Get(fmt
.Sprintf("http://%s/", ln
.Addr()))
711 t
.Fatalf("http client error: %v", err
)
713 _
, err
= io
.Copy(ioutil
.Discard
, res
.Body
)
715 t
.Fatalf("client body read error: %v", err
)
719 // Force child to exit cleanly.
720 Get(fmt
.Sprintf("http://%s/quit", ln
.Addr()))
723 rx
:= regexp
.MustCompile(`sendfile(64)?\(\d+,\s*\d+,\s*NULL,\s*\d+\)\s*=\s*\d+\s*\n`)
724 rxResume
:= regexp
.MustCompile(`<\.\.\. sendfile(64)? resumed> \)\s*=\s*\d+\s*\n`)
726 if !rx
.MatchString(out
) && !rxResume
.MatchString(out
) {
727 t
.Errorf("no sendfile system call found in:\n%s", out
)
731 func getBody(t
*testing
.T
, testName
string, req Request
) (*Response
, []byte) {
732 r
, err
:= DefaultClient
.Do(&req
)
734 t
.Fatalf("%s: for URL %q, send error: %v", testName
, req
.URL
.String(), err
)
736 b
, err
:= ioutil
.ReadAll(r
.Body
)
738 t
.Fatalf("%s: for URL %q, reading body: %v", testName
, req
.URL
.String(), err
)
743 // TestLinuxSendfileChild isn't a real test. It's used as a helper process
744 // for TestLinuxSendfile.
745 func TestLinuxSendfileChild(*testing
.T
) {
746 if os
.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
750 fd3
:= os
.NewFile(3, "ephemeral-port-listener")
751 ln
, err
:= net
.FileListener(fd3
)
756 mux
.Handle("/", FileServer(Dir("testdata")))
757 mux
.HandleFunc("/quit", func(ResponseWriter
, *Request
) {
760 s
:= &Server
{Handler
: mux
}