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.
33 testFile
= "testdata/file"
37 type wantRange
struct {
38 start
, end
int64 // range [start,end)
41 var ServeFileRangeTests
= []struct {
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
},
70 func TestServeFile(t
*testing
.T
) {
73 ts
:= httptest
.NewServer(HandlerFunc(func(w ResponseWriter
, r
*Request
) {
74 ServeFile(w
, r
, "testdata/file")
80 file
, err
:= ioutil
.ReadFile(testFile
)
82 t
.Fatal("reading file:", err
)
85 // set up the Request (re-used for all tests)
87 req
.Header
= make(Header
)
88 if req
.URL
, err
= url
.Parse(ts
.URL
); err
!= nil {
89 t
.Fatal("ParseURL:", err
)
94 _
, body
:= getBody(t
, "straight get", req
)
95 if !bytes
.Equal(body
, file
) {
96 t
.Fatalf("body mismatch: got %q, want %q", body
, file
)
101 for _
, rt
:= range ServeFileRangeTests
{
103 req
.Header
.Set("Range", rt
.r
)
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
)
109 if rt
.code
== StatusRequestedRangeNotSatisfiable
{
112 wantContentRange
:= ""
113 if len(rt
.ranges
) == 1 {
115 wantContentRange
= fmt
.Sprintf("bytes %d-%d/%d", rng
.start
, rng
.end
-1, testFileLen
)
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
)
121 ct
:= resp
.Header
.Get("Content-Type")
122 if len(rt
.ranges
) == 1 {
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
)
128 if strings
.HasPrefix(ct
, "multipart/byteranges") {
129 t
.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt
.r
, ct
)
132 if len(rt
.ranges
) > 1 {
133 typ
, params
, err
:= mime
.ParseMediaType(ct
)
135 t
.Errorf("range=%q content-type = %q; %v", rt
.r
, ct
, err
)
138 if typ
!= "multipart/byteranges" {
139 t
.Errorf("range=%q content-type = %q; want multipart/byteranges", rt
.r
, typ
)
142 if params
["boundary"] == "" {
143 t
.Errorf("range=%q content-type = %q; lacks boundary", rt
.r
, ct
)
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
)
150 mr
:= multipart
.NewReader(bytes
.NewReader(body
), params
["boundary"])
151 for ri
, rng
:= range rt
.ranges
{
152 part
, err
:= mr
.NextPart()
154 t
.Errorf("range=%q, reading part index %d: %v", rt
.r
, ri
, err
)
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
)
161 body
, err
:= ioutil
.ReadAll(part
)
163 t
.Errorf("range=%q, reading part index %d body: %v", rt
.r
, ri
, err
)
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
)
171 _
, err
= mr
.NextPart()
173 t
.Errorf("range=%q; expected final error io.EOF; got %v", rt
.r
, err
)
179 func TestServeFile_DotDot(t
*testing
.T
) {
184 {"/testdata/file", 200},
193 {"/file/a\\..", 400},
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")))
198 t
.Errorf("bad request %q: %v", tt
.req
, err
)
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
)
209 var fsRedirectTestData
= []struct {
210 original
, redirect
string
212 {"/test/index.html", "/test/"},
213 {"/test/testdata", "/test/testdata/"},
214 {"/test/testdata/file/", "/test/testdata/file"},
217 func TestFSRedirect(t
*testing
.T
) {
219 ts
:= httptest
.NewServer(StripPrefix("/test", FileServer(Dir("."))))
222 for _
, data
:= range fsRedirectTestData
{
223 res
, err
:= Get(ts
.URL
+ data
.original
)
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
)
234 type testFileSystem
struct {
235 open
func(name
string) (File
, error
)
238 func (fs
*testFileSystem
) Open(name
string) (File
, error
) {
242 func TestFileServerCleans(t
*testing
.T
) {
244 ch
:= make(chan string, 1)
245 fs
:= FileServer(&testFileSystem
{func(name
string) (File
, error
) {
247 return nil, errors
.New("file does not exist")
250 reqPath
, openArg
string
252 {"/foo.txt", "/foo.txt"},
253 {"//foo.txt", "/foo.txt"},
254 {"/../foo.txt", "/foo.txt"},
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
)
267 func TestFileServerEscapesNames(t
*testing
.T
) {
269 const dirListPrefix
= "<pre>\n"
270 const dirListSuffix
= "\n</pre>\n"
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>`},
281 // We put each test file in its own directory in the fakeFS so we can look at it in isolation.
283 for i
, test
:= range tests
{
284 testFile
:= &fakeFileInfo
{basename
: test
.name
}
285 fs
[fmt
.Sprintf("/%d", i
)] = &fakeFileInfo
{
287 modtime
: time
.Unix(1000000000, 0).UTC(),
288 ents
: []*fakeFileInfo
{testFile
},
290 fs
[fmt
.Sprintf("/%d/%s", i
, test
.name
)] = testFile
293 ts
:= httptest
.NewServer(FileServer(&fs
))
295 for i
, test
:= range tests
{
296 url
:= fmt
.Sprintf("%s/%d", ts
.URL
, i
)
299 t
.Fatalf("test %q: Get: %v", test
.name
, err
)
301 b
, err
:= ioutil
.ReadAll(res
.Body
)
303 t
.Fatalf("test %q: read Body: %v", test
.name
, err
)
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
)
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
)
316 func TestFileServerSortsNames(t
*testing
.T
) {
318 const contents
= "I am a fake file"
319 dirMod
:= time
.Unix(123, 0).UTC()
320 fileMod
:= time
.Unix(1000000000, 0).UTC()
325 ents
: []*fakeFileInfo
{
340 ts
:= httptest
.NewServer(FileServer(&fs
))
343 res
, err
:= Get(ts
.URL
)
345 t
.Fatalf("Get: %v", err
)
347 defer res
.Body
.Close()
349 b
, err
:= ioutil
.ReadAll(res
.Body
)
351 t
.Fatalf("read Body: %v", err
)
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
)
359 func mustRemoveAll(dir
string) {
360 err
:= os
.RemoveAll(dir
)
366 func TestFileServerImplicitLeadingSlash(t
*testing
.T
) {
368 tempDir
, err
:= ioutil
.TempDir("", "")
370 t
.Fatalf("TempDir: %v", err
)
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
)
376 ts
:= httptest
.NewServer(StripPrefix("/bar/", FileServer(Dir(tempDir
))))
378 get
:= func(suffix
string) string {
379 res
, err
:= Get(ts
.URL
+ suffix
)
381 t
.Fatalf("Get %s: %v", suffix
, err
)
383 b
, err
:= ioutil
.ReadAll(res
.Body
)
385 t
.Fatalf("ReadAll %s: %v", suffix
, err
)
390 if s
:= get("/bar/"); !strings
.Contains(s
, ">foo.txt<") {
391 t
.Logf("expected a directory listing with foo.txt, got %q", s
)
393 if s
:= get("/bar/foo.txt"); s
!= "Hello world" {
394 t
.Logf("expected %q, got %q", "Hello world", s
)
398 func TestDirJoin(t
*testing
.T
) {
399 if runtime
.GOOS
== "windows" {
400 t
.Skip("skipping test on windows")
402 wfi
, err
:= os
.Stat("/etc/hosts")
404 t
.Skip("skipping test; no /etc/hosts file")
406 test
:= func(d Dir
, name
string) {
407 f
, err
:= d
.Open(name
)
409 t
.Fatalf("open of %s: %v", name
, err
)
414 t
.Fatalf("stat of %s: %v", name
, err
)
416 if !os
.SameFile(gfi
, wfi
) {
417 t
.Errorf("%s got different file", name
)
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")
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"), "../")
434 func TestEmptyDirOpenCWD(t
*testing
.T
) {
435 test
:= func(d Dir
) {
437 f
, err
:= d
.Open(name
)
439 t
.Fatalf("open of %s: %v", name
, err
)
448 func TestServeFileContentType(t
*testing
.T
) {
450 const ctype
= "icecream/chocolate"
451 ts
:= httptest
.NewServer(HandlerFunc(func(w ResponseWriter
, r
*Request
) {
452 switch r
.FormValue("override") {
454 w
.Header().Set("Content-Type", ctype
)
456 // Explicitly inhibit sniffing.
457 w
.Header()["Content-Type"] = []string{}
459 ServeFile(w
, r
, "testdata/file")
462 get
:= func(override
string, want
[]string) {
463 resp
, err
:= Get(ts
.URL
+ "?override=" + override
)
467 if h
:= resp
.Header
["Content-Type"]; !reflect
.DeepEqual(h
, want
) {
468 t
.Errorf("Content-Type mismatch: got %v, want %v", h
, want
)
472 get("0", []string{"text/plain; charset=utf-8"})
473 get("1", []string{ctype
})
477 func TestServeFileMimeType(t
*testing
.T
) {
479 ts
:= httptest
.NewServer(HandlerFunc(func(w ResponseWriter
, r
*Request
) {
480 ServeFile(w
, r
, "testdata/style.css")
483 resp
, err
:= Get(ts
.URL
)
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
)
494 func TestServeFileFromCWD(t
*testing
.T
) {
496 ts
:= httptest
.NewServer(HandlerFunc(func(w ResponseWriter
, r
*Request
) {
497 ServeFile(w
, r
, "fs_test.go")
500 r
, err
:= Get(ts
.URL
)
505 if r
.StatusCode
!= 200 {
506 t
.Fatalf("expected 200 OK, got %s", r
.Status
)
511 func TestServeDirWithoutTrailingSlash(t
*testing
.T
) {
514 ts
:= httptest
.NewServer(HandlerFunc(func(w ResponseWriter
, r
*Request
) {
518 r
, err
:= Get(ts
.URL
+ "/testdata")
523 if g
:= r
.Request
.URL
.Path
; g
!= e
{
524 t
.Errorf("got %s, want %s", g
, e
)
528 // Tests that ServeFile doesn't add a Content-Length if a Content-Encoding is
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) {
534 cst
:= newClientServerTest(t
, h2
, HandlerFunc(func(w ResponseWriter
, r
*Request
) {
535 w
.Header().Set("Content-Encoding", "foo")
536 ServeFile(w
, r
, "testdata/file")
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.
548 resp
, err
:= cst
.c
.Get(cst
.ts
.URL
)
553 if g
, e
:= resp
.ContentLength
, int64(-1); g
!= e
{
554 t
.Errorf("Content-Length mismatch: got %d, want %d", g
, e
)
558 func TestServeIndexHtml(t
*testing
.T
) {
560 const want
= "index.html says hello\n"
561 ts
:= httptest
.NewServer(FileServer(Dir(".")))
564 for _
, path
:= range []string{"/testdata/", "/testdata/index.html"} {
565 res
, err
:= Get(ts
.URL
+ path
)
569 b
, err
:= ioutil
.ReadAll(res
.Body
)
571 t
.Fatal("reading Body:", err
)
573 if s
:= string(b
); s
!= want
{
574 t
.Errorf("for path %q got %q, want %q", path
, s
, want
)
580 func TestFileServerZeroByte(t
*testing
.T
) {
582 ts
:= httptest
.NewServer(FileServer(Dir(".")))
585 res
, err
:= Get(ts
.URL
+ "/..\x00")
589 b
, err
:= ioutil
.ReadAll(res
.Body
)
591 t
.Fatal("reading Body:", err
)
593 if res
.StatusCode
== 200 {
594 t
.Errorf("got status 200; want an error. Body is:\n%s", string(b
))
598 type fakeFileInfo
struct {
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
{
614 return 0755 | os
.ModeDir
619 type fakeFile
struct {
622 path
string // as opened
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
) {
630 return nil, os
.ErrInvalid
632 var fis
[]os
.FileInfo
634 limit
:= f
.entpos
+ count
635 if count
<= 0 || limit
> len(f
.fi
.ents
) {
636 limit
= len(f
.fi
.ents
)
638 for ; f
.entpos
< limit
; f
.entpos
++ {
639 fis
= append(fis
, f
.fi
.ents
[f
.entpos
])
642 if len(fis
) == 0 && count
> 0 {
649 type fakeFS
map[string]*fakeFileInfo
651 func (fs fakeFS
) Open(name
string) (File
, error
) {
652 name
= path
.Clean(name
)
655 return nil, os
.ErrNotExist
660 return &fakeFile
{ReadSeeker
: strings
.NewReader(f
.contents
), fi
: f
, path
: name
}, nil
663 func TestDirectoryIfNotModified(t
*testing
.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",
672 contents
: indexContents
,
678 ents
: []*fakeFileInfo
{indexFile
},
680 "/index.html": indexFile
,
683 ts
:= httptest
.NewServer(FileServer(fs
))
686 res
, err
:= Get(ts
.URL
)
690 b
, err
:= ioutil
.ReadAll(res
.Body
)
694 if string(b
) != indexContents
{
695 t
.Fatalf("Got body %q; want %q", b
, indexContents
)
699 lastMod
:= res
.Header
.Get("Last-Modified")
700 if lastMod
!= fileModStr
{
701 t
.Fatalf("initial Last-Modified = %q; want %q", lastMod
, fileModStr
)
704 req
, _
:= NewRequest("GET", ts
.URL
, nil)
705 req
.Header
.Set("If-Modified-Since", lastMod
)
707 res
, err
= DefaultClient
.Do(req
)
711 if res
.StatusCode
!= 304 {
712 t
.Fatalf("Code after If-Modified-Since request = %v; want 304", res
.StatusCode
)
716 // Advance the index.html file's modtime, but not the directory's.
717 indexFile
.modtime
= indexFile
.modtime
.Add(1 * time
.Hour
)
719 res
, err
= DefaultClient
.Do(req
)
723 if res
.StatusCode
!= 200 {
724 t
.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res
.StatusCode
, res
)
729 func mustStat(t
*testing
.T
, fileName
string) os
.FileInfo
{
730 fi
, err
:= os
.Stat(fileName
)
737 func TestServeContent(t
*testing
.T
) {
739 type serveParam
struct {
742 content io
.ReadSeeker
746 servec
:= make(chan serveParam
, 1)
747 ts
:= httptest
.NewServer(HandlerFunc(func(w ResponseWriter
, r
*Request
) {
750 w
.Header().Set("ETag", p
.etag
)
752 if p
.contentType
!= "" {
753 w
.Header().Set("Content-Type", p
.contentType
)
755 ServeContent(w
, r
, p
.name
, p
.modtime
, p
.content
)
759 type testCase
struct {
760 // One of file or content must be set:
762 content io
.ReadSeeker
765 serveETag
string // optional
766 serveContentType
string // optional
767 reqHeader
map[string]string
769 wantContentType
string
770 wantContentRange
string
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",
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
),
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
),
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
),
806 "not_modified_etag": {
807 file
: "testdata/style.css",
809 reqHeader
: map[string]string{
810 "If-None-Match": `"foo"`,
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"`,
822 "if_none_match_mismatch": {
823 file
: "testdata/style.css",
825 reqHeader
: map[string]string{
826 "If-None-Match": `"Foo"`,
829 wantContentType
: "text/css; charset=utf-8",
832 file
: "testdata/style.css",
834 reqHeader
: map[string]string{
835 "Range": "bytes=0-4",
837 wantStatus
: StatusPartialContent
,
838 wantContentType
: "text/css; charset=utf-8",
839 wantContentRange
: "bytes 0-4/8",
842 file
: "testdata/style.css",
844 reqHeader
: map[string]string{
845 "Range": "bytes=0-4",
848 wantStatus
: StatusPartialContent
,
849 wantContentType
: "text/css; charset=utf-8",
850 wantContentRange
: "bytes 0-4/8",
852 "range_match_weak_etag": {
853 file
: "testdata/style.css",
855 reqHeader
: map[string]string{
856 "Range": "bytes=0-4",
860 wantContentType
: "text/css; charset=utf-8",
862 "range_no_overlap": {
863 file
: "testdata/style.css",
865 reqHeader
: map[string]string{
866 "Range": "bytes=10-20",
868 wantStatus
: StatusRequestedRangeNotSatisfiable
,
869 wantContentType
: "text/plain; charset=utf-8",
870 wantContentRange
: "bytes */8",
872 // An If-Range resource for entity "A", but entity "B" is now current.
873 // The Range request should be ignored.
875 file
: "testdata/style.css",
877 reqHeader
: map[string]string{
878 "Range": "bytes=0-4",
882 wantContentType
: "text/css; charset=utf-8",
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",
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",
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",
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",
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",
915 file
: "testdata/style.css",
917 reqHeader
: map[string]string{
918 "If-Match": `"Z", "A"`,
921 wantContentType
: "text/css; charset=utf-8",
924 file
: "testdata/style.css",
926 reqHeader
: map[string]string{
930 wantContentType
: "text/css; charset=utf-8",
933 file
: "testdata/style.css",
935 reqHeader
: map[string]string{
939 wantContentType
: "text/plain; charset=utf-8",
941 "ifmatch_fails_on_weak_etag": {
942 file
: "testdata/style.css",
944 reqHeader
: map[string]string{
948 wantContentType
: "text/plain; charset=utf-8",
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
),
957 wantContentType
: "text/css; charset=utf-8",
958 wantLastMod
: htmlModTime
.UTC().Format(TimeFormat
),
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
),
967 wantContentType
: "text/plain; charset=utf-8",
968 wantLastMod
: htmlModTime
.UTC().Format(TimeFormat
),
971 for testName
, tt
:= range tests
{
972 var content io
.ReadSeeker
974 f
, err
:= os
.Open(tt
.file
)
976 t
.Fatalf("test %q: %v", testName
, err
)
984 servec
<- serveParam
{
985 name
: filepath
.Base(tt
.file
),
989 contentType
: tt
.serveContentType
,
991 req
, err
:= NewRequest("GET", ts
.URL
, nil)
995 for k
, v
:= range tt
.reqHeader
{
998 res
, err
:= DefaultClient
.Do(req
)
1002 io
.Copy(ioutil
.Discard
, res
.Body
)
1004 if res
.StatusCode
!= tt
.wantStatus
{
1005 t
.Errorf("test %q: status = %d; want %d", testName
, res
.StatusCode
, tt
.wantStatus
)
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
)
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
)
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
)
1020 func TestServerFileStatError(t
*testing
.T
) {
1021 rec
:= httptest
.NewRecorder()
1022 r
, _
:= NewRequest("GET", "http://foo/", nil)
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
)
1032 type issue12991FS
struct{}
1034 func (issue12991FS
) Open(string) (File
, error
) { return issue12991File
{}, nil }
1036 type issue12991File
struct{ File
}
1038 func (issue12991File
) Stat() (os
.FileInfo
, error
) { return nil, os
.ErrPermission
}
1039 func (issue12991File
) Close() error
{ return nil }
1041 func TestServeContentErrorMessages(t
*testing
.T
) {
1044 "/500": &fakeFileInfo
{
1045 err
: errors
.New("random error"),
1047 "/403": &fakeFileInfo
{
1048 err
: &os
.PathError
{Err
: os
.ErrPermission
},
1051 ts
:= httptest
.NewServer(FileServer(fs
))
1053 for _
, code
:= range []int{403, 404, 500} {
1054 res
, err
:= DefaultClient
.Get(fmt
.Sprintf("%s/%d", ts
.URL
, code
))
1056 t
.Errorf("Error fetching /%d: %v", code
, err
)
1059 if res
.StatusCode
!= code
{
1060 t
.Errorf("For /%d, status code = %d; want %d", code
, res
.StatusCode
, code
)
1066 // verifies that sendfile is being used on Linux
1067 func TestLinuxSendfile(t
*testing
.T
) {
1070 if runtime
.GOOS
!= "linux" {
1071 t
.Skip("skipping; linux-only test")
1073 if _
, err
:= exec
.LookPath("strace"); err
!= nil {
1074 t
.Skip("skipping; strace not found in path")
1077 ln
, err
:= net
.Listen("tcp", "127.0.0.1:0")
1081 lnf
, err
:= ln
.(*net
.TCPListener
).File()
1087 syscalls
:= "sendfile,sendfile64"
1088 switch runtime
.GOARCH
{
1089 case "mips64", "mips64le", "s390x", "alpha":
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"
1094 t
.Skip("TODO: update this test to be robust against various versions of strace on mips64. See golang.org/issue/33430")
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()...)
1103 if err
:= child
.Start(); err
!= nil {
1104 t
.Skipf("skipping; failed to start straced child: %v", err
)
1107 res
, err
:= Get(fmt
.Sprintf("http://%s/", ln
.Addr()))
1109 t
.Fatalf("http client error: %v", err
)
1111 _
, err
= io
.Copy(ioutil
.Discard
, res
.Body
)
1113 t
.Fatalf("client body read error: %v", err
)
1117 // Force child to exit cleanly.
1118 Post(fmt
.Sprintf("http://%s/quit", ln
.Addr()), "", nil)
1121 rx
:= regexp
.MustCompile(`sendfile(64)?\(\d+,\s*\d+,\s*NULL,\s*\d+`)
1123 if !rx
.MatchString(out
) {
1124 t
.Errorf("no sendfile system call found in:\n%s", out
)
1128 func getBody(t
*testing
.T
, testName
string, req Request
) (*Response
, []byte) {
1129 r
, err
:= DefaultClient
.Do(&req
)
1131 t
.Fatalf("%s: for URL %q, send error: %v", testName
, req
.URL
.String(), err
)
1133 b
, err
:= ioutil
.ReadAll(r
.Body
)
1135 t
.Fatalf("%s: for URL %q, reading body: %v", testName
, req
.URL
.String(), err
)
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" {
1147 fd3
:= os
.NewFile(3, "ephemeral-port-listener")
1148 ln
, err
:= net
.FileListener(fd3
)
1152 mux
:= NewServeMux()
1153 mux
.Handle("/", FileServer(Dir("testdata")))
1154 mux
.HandleFunc("/quit", func(ResponseWriter
, *Request
) {
1157 s
:= &Server
{Handler
: mux
}
1164 func TestFileServerCleanPath(t
*testing
.T
) {
1170 {"/", 200, []string{"/", "/index.html"}},
1171 {"/dir", 301, []string{"/dir"}},
1172 {"/dir/", 200, []string{"/dir", "/dir/index.html"}},
1174 for _
, tt
:= range tests
{
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
)
1182 if rr
.Code
!= tt
.wantCode
{
1183 t
.Logf("For %s: Response code = %d; want %d", tt
.path
, rr
.Code
, tt
.wantCode
)
1188 type fileServerCleanPathDir
struct {
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(".")
1198 return nil, os
.ErrNotExist
1201 type panicOnSeek
struct{ io
.ReadSeeker
}
1203 func Test_scanETag(t
*testing
.T
) {
1209 {`W/"etag-1"`, `W/"etag-1"`, ""},
1210 {`"etag-2"`, `"etag-2"`, ""},
1211 {`"etag-1", "etag-2"`, `"etag-1"`, `, "etag-2"`},
1215 {`W/"truc`, "", ""},
1216 {`w/"case-sensitive"`, "", ""},
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
)