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")
81 file
, err
:= ioutil
.ReadFile(testFile
)
83 t
.Fatal("reading file:", err
)
86 // set up the Request (re-used for all tests)
88 req
.Header
= make(Header
)
89 if req
.URL
, err
= url
.Parse(ts
.URL
); err
!= nil {
90 t
.Fatal("ParseURL:", err
)
95 _
, body
:= getBody(t
, "straight get", req
, c
)
96 if !bytes
.Equal(body
, file
) {
97 t
.Fatalf("body mismatch: got %q, want %q", body
, file
)
102 for _
, rt
:= range ServeFileRangeTests
{
104 req
.Header
.Set("Range", rt
.r
)
106 resp
, body
:= getBody(t
, fmt
.Sprintf("range test %q", rt
.r
), req
, c
)
107 if resp
.StatusCode
!= rt
.code
{
108 t
.Errorf("range=%q: StatusCode=%d, want %d", rt
.r
, resp
.StatusCode
, rt
.code
)
110 if rt
.code
== StatusRequestedRangeNotSatisfiable
{
113 wantContentRange
:= ""
114 if len(rt
.ranges
) == 1 {
116 wantContentRange
= fmt
.Sprintf("bytes %d-%d/%d", rng
.start
, rng
.end
-1, testFileLen
)
118 cr
:= resp
.Header
.Get("Content-Range")
119 if cr
!= wantContentRange
{
120 t
.Errorf("range=%q: Content-Range = %q, want %q", rt
.r
, cr
, wantContentRange
)
122 ct
:= resp
.Header
.Get("Content-Type")
123 if len(rt
.ranges
) == 1 {
125 wantBody
:= file
[rng
.start
:rng
.end
]
126 if !bytes
.Equal(body
, wantBody
) {
127 t
.Errorf("range=%q: body = %q, want %q", rt
.r
, body
, wantBody
)
129 if strings
.HasPrefix(ct
, "multipart/byteranges") {
130 t
.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt
.r
, ct
)
133 if len(rt
.ranges
) > 1 {
134 typ
, params
, err
:= mime
.ParseMediaType(ct
)
136 t
.Errorf("range=%q content-type = %q; %v", rt
.r
, ct
, err
)
139 if typ
!= "multipart/byteranges" {
140 t
.Errorf("range=%q content-type = %q; want multipart/byteranges", rt
.r
, typ
)
143 if params
["boundary"] == "" {
144 t
.Errorf("range=%q content-type = %q; lacks boundary", rt
.r
, ct
)
147 if g
, w
:= resp
.ContentLength
, int64(len(body
)); g
!= w
{
148 t
.Errorf("range=%q Content-Length = %d; want %d", rt
.r
, g
, w
)
151 mr
:= multipart
.NewReader(bytes
.NewReader(body
), params
["boundary"])
152 for ri
, rng
:= range rt
.ranges
{
153 part
, err
:= mr
.NextPart()
155 t
.Errorf("range=%q, reading part index %d: %v", rt
.r
, ri
, err
)
158 wantContentRange
= fmt
.Sprintf("bytes %d-%d/%d", rng
.start
, rng
.end
-1, testFileLen
)
159 if g
, w
:= part
.Header
.Get("Content-Range"), wantContentRange
; g
!= w
{
160 t
.Errorf("range=%q: part Content-Range = %q; want %q", rt
.r
, g
, w
)
162 body
, err
:= ioutil
.ReadAll(part
)
164 t
.Errorf("range=%q, reading part index %d body: %v", rt
.r
, ri
, err
)
167 wantBody
:= file
[rng
.start
:rng
.end
]
168 if !bytes
.Equal(body
, wantBody
) {
169 t
.Errorf("range=%q: body = %q, want %q", rt
.r
, body
, wantBody
)
172 _
, err
= mr
.NextPart()
174 t
.Errorf("range=%q; expected final error io.EOF; got %v", rt
.r
, err
)
180 func TestServeFile_DotDot(t
*testing
.T
) {
185 {"/testdata/file", 200},
194 {"/file/a\\..", 400},
196 for _
, tt
:= range tests
{
197 req
, err
:= ReadRequest(bufio
.NewReader(strings
.NewReader("GET " + tt
.req
+ " HTTP/1.1\r\nHost: foo\r\n\r\n")))
199 t
.Errorf("bad request %q: %v", tt
.req
, err
)
202 rec
:= httptest
.NewRecorder()
203 ServeFile(rec
, req
, "testdata/file")
204 if rec
.Code
!= tt
.wantStatus
{
205 t
.Errorf("for request %q, status = %d; want %d", tt
.req
, rec
.Code
, tt
.wantStatus
)
210 var fsRedirectTestData
= []struct {
211 original
, redirect
string
213 {"/test/index.html", "/test/"},
214 {"/test/testdata", "/test/testdata/"},
215 {"/test/testdata/file/", "/test/testdata/file"},
218 func TestFSRedirect(t
*testing
.T
) {
220 ts
:= httptest
.NewServer(StripPrefix("/test", FileServer(Dir("."))))
223 for _
, data
:= range fsRedirectTestData
{
224 res
, err
:= Get(ts
.URL
+ data
.original
)
229 if g
, e
:= res
.Request
.URL
.Path
, data
.redirect
; g
!= e
{
230 t
.Errorf("redirect from %s: got %s, want %s", data
.original
, g
, e
)
235 type testFileSystem
struct {
236 open
func(name
string) (File
, error
)
239 func (fs
*testFileSystem
) Open(name
string) (File
, error
) {
243 func TestFileServerCleans(t
*testing
.T
) {
245 ch
:= make(chan string, 1)
246 fs
:= FileServer(&testFileSystem
{func(name
string) (File
, error
) {
248 return nil, errors
.New("file does not exist")
251 reqPath
, openArg
string
253 {"/foo.txt", "/foo.txt"},
254 {"//foo.txt", "/foo.txt"},
255 {"/../foo.txt", "/foo.txt"},
257 req
, _
:= NewRequest("GET", "http://example.com", nil)
258 for n
, test
:= range tests
{
259 rec
:= httptest
.NewRecorder()
260 req
.URL
.Path
= test
.reqPath
261 fs
.ServeHTTP(rec
, req
)
262 if got
:= <-ch
; got
!= test
.openArg
{
263 t
.Errorf("test %d: got %q, want %q", n
, got
, test
.openArg
)
268 func TestFileServerEscapesNames(t
*testing
.T
) {
270 const dirListPrefix
= "<pre>\n"
271 const dirListSuffix
= "\n</pre>\n"
275 {`simple_name`, `<a href="simple_name">simple_name</a>`},
276 {`"'<>&`, `<a href="%22%27%3C%3E&">"'<>&</a>`},
277 {`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`},
278 {`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo"><combo>?foo</a>`},
279 {`foo:bar`, `<a href="./foo:bar">foo:bar</a>`},
282 // We put each test file in its own directory in the fakeFS so we can look at it in isolation.
284 for i
, test
:= range tests
{
285 testFile
:= &fakeFileInfo
{basename
: test
.name
}
286 fs
[fmt
.Sprintf("/%d", i
)] = &fakeFileInfo
{
288 modtime
: time
.Unix(1000000000, 0).UTC(),
289 ents
: []*fakeFileInfo
{testFile
},
291 fs
[fmt
.Sprintf("/%d/%s", i
, test
.name
)] = testFile
294 ts
:= httptest
.NewServer(FileServer(&fs
))
296 for i
, test
:= range tests
{
297 url
:= fmt
.Sprintf("%s/%d", ts
.URL
, i
)
300 t
.Fatalf("test %q: Get: %v", test
.name
, err
)
302 b
, err
:= ioutil
.ReadAll(res
.Body
)
304 t
.Fatalf("test %q: read Body: %v", test
.name
, err
)
307 if !strings
.HasPrefix(s
, dirListPrefix
) ||
!strings
.HasSuffix(s
, dirListSuffix
) {
308 t
.Errorf("test %q: listing dir, full output is %q, want prefix %q and suffix %q", test
.name
, s
, dirListPrefix
, dirListSuffix
)
310 if trimmed
:= strings
.TrimSuffix(strings
.TrimPrefix(s
, dirListPrefix
), dirListSuffix
); trimmed
!= test
.escaped
{
311 t
.Errorf("test %q: listing dir, filename escaped to %q, want %q", test
.name
, trimmed
, test
.escaped
)
317 func TestFileServerSortsNames(t
*testing
.T
) {
319 const contents
= "I am a fake file"
320 dirMod
:= time
.Unix(123, 0).UTC()
321 fileMod
:= time
.Unix(1000000000, 0).UTC()
326 ents
: []*fakeFileInfo
{
341 ts
:= httptest
.NewServer(FileServer(&fs
))
344 res
, err
:= Get(ts
.URL
)
346 t
.Fatalf("Get: %v", err
)
348 defer res
.Body
.Close()
350 b
, err
:= ioutil
.ReadAll(res
.Body
)
352 t
.Fatalf("read Body: %v", err
)
355 if !strings
.Contains(s
, "<a href=\"a\">a</a>\n<a href=\"b\">b</a>") {
356 t
.Errorf("output appears to be unsorted:\n%s", s
)
360 func mustRemoveAll(dir
string) {
361 err
:= os
.RemoveAll(dir
)
367 func TestFileServerImplicitLeadingSlash(t
*testing
.T
) {
369 tempDir
, err
:= ioutil
.TempDir("", "")
371 t
.Fatalf("TempDir: %v", err
)
373 defer mustRemoveAll(tempDir
)
374 if err
:= ioutil
.WriteFile(filepath
.Join(tempDir
, "foo.txt"), []byte("Hello world"), 0644); err
!= nil {
375 t
.Fatalf("WriteFile: %v", err
)
377 ts
:= httptest
.NewServer(StripPrefix("/bar/", FileServer(Dir(tempDir
))))
379 get
:= func(suffix
string) string {
380 res
, err
:= Get(ts
.URL
+ suffix
)
382 t
.Fatalf("Get %s: %v", suffix
, err
)
384 b
, err
:= ioutil
.ReadAll(res
.Body
)
386 t
.Fatalf("ReadAll %s: %v", suffix
, err
)
391 if s
:= get("/bar/"); !strings
.Contains(s
, ">foo.txt<") {
392 t
.Logf("expected a directory listing with foo.txt, got %q", s
)
394 if s
:= get("/bar/foo.txt"); s
!= "Hello world" {
395 t
.Logf("expected %q, got %q", "Hello world", s
)
399 func TestDirJoin(t
*testing
.T
) {
400 if runtime
.GOOS
== "windows" {
401 t
.Skip("skipping test on windows")
403 wfi
, err
:= os
.Stat("/etc/hosts")
405 t
.Skip("skipping test; no /etc/hosts file")
407 test
:= func(d Dir
, name
string) {
408 f
, err
:= d
.Open(name
)
410 t
.Fatalf("open of %s: %v", name
, err
)
415 t
.Fatalf("stat of %s: %v", name
, err
)
417 if !os
.SameFile(gfi
, wfi
) {
418 t
.Errorf("%s got different file", name
)
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 test(Dir("/etc"), "../../../../hosts")
428 // Not really directories, but since we use this trick in
429 // ServeFile, test it:
430 test(Dir("/etc/hosts"), "")
431 test(Dir("/etc/hosts"), "/")
432 test(Dir("/etc/hosts"), "../")
435 func TestEmptyDirOpenCWD(t
*testing
.T
) {
436 test
:= func(d Dir
) {
438 f
, err
:= d
.Open(name
)
440 t
.Fatalf("open of %s: %v", name
, err
)
449 func TestServeFileContentType(t
*testing
.T
) {
451 const ctype
= "icecream/chocolate"
452 ts
:= httptest
.NewServer(HandlerFunc(func(w ResponseWriter
, r
*Request
) {
453 switch r
.FormValue("override") {
455 w
.Header().Set("Content-Type", ctype
)
457 // Explicitly inhibit sniffing.
458 w
.Header()["Content-Type"] = []string{}
460 ServeFile(w
, r
, "testdata/file")
463 get
:= func(override
string, want
[]string) {
464 resp
, err
:= Get(ts
.URL
+ "?override=" + override
)
468 if h
:= resp
.Header
["Content-Type"]; !reflect
.DeepEqual(h
, want
) {
469 t
.Errorf("Content-Type mismatch: got %v, want %v", h
, want
)
473 get("0", []string{"text/plain; charset=utf-8"})
474 get("1", []string{ctype
})
478 func TestServeFileMimeType(t
*testing
.T
) {
480 ts
:= httptest
.NewServer(HandlerFunc(func(w ResponseWriter
, r
*Request
) {
481 ServeFile(w
, r
, "testdata/style.css")
484 resp
, err
:= Get(ts
.URL
)
489 want
:= "text/css; charset=utf-8"
490 if h
:= resp
.Header
.Get("Content-Type"); h
!= want
{
491 t
.Errorf("Content-Type mismatch: got %q, want %q", h
, want
)
495 func TestServeFileFromCWD(t
*testing
.T
) {
497 ts
:= httptest
.NewServer(HandlerFunc(func(w ResponseWriter
, r
*Request
) {
498 ServeFile(w
, r
, "fs_test.go")
501 r
, err
:= Get(ts
.URL
)
506 if r
.StatusCode
!= 200 {
507 t
.Fatalf("expected 200 OK, got %s", r
.Status
)
512 func TestServeDirWithoutTrailingSlash(t
*testing
.T
) {
515 ts
:= httptest
.NewServer(HandlerFunc(func(w ResponseWriter
, r
*Request
) {
519 r
, err
:= Get(ts
.URL
+ "/testdata")
524 if g
:= r
.Request
.URL
.Path
; g
!= e
{
525 t
.Errorf("got %s, want %s", g
, e
)
529 // Tests that ServeFile doesn't add a Content-Length if a Content-Encoding is
531 func TestServeFileWithContentEncoding_h1(t
*testing
.T
) { testServeFileWithContentEncoding(t
, h1Mode
) }
532 func TestServeFileWithContentEncoding_h2(t
*testing
.T
) { testServeFileWithContentEncoding(t
, h2Mode
) }
533 func testServeFileWithContentEncoding(t
*testing
.T
, h2
bool) {
535 cst
:= newClientServerTest(t
, h2
, HandlerFunc(func(w ResponseWriter
, r
*Request
) {
536 w
.Header().Set("Content-Encoding", "foo")
537 ServeFile(w
, r
, "testdata/file")
539 // Because the testdata is so small, it would fit in
540 // both the h1 and h2 Server's write buffers. For h1,
541 // sendfile is used, though, forcing a header flush at
542 // the io.Copy. http2 doesn't do a header flush so
543 // buffers all 11 bytes and then adds its own
544 // Content-Length. To prevent the Server's
545 // Content-Length and test ServeFile only, flush here.
549 resp
, err
:= cst
.c
.Get(cst
.ts
.URL
)
554 if g
, e
:= resp
.ContentLength
, int64(-1); g
!= e
{
555 t
.Errorf("Content-Length mismatch: got %d, want %d", g
, e
)
559 func TestServeIndexHtml(t
*testing
.T
) {
561 const want
= "index.html says hello\n"
562 ts
:= httptest
.NewServer(FileServer(Dir(".")))
565 for _
, path
:= range []string{"/testdata/", "/testdata/index.html"} {
566 res
, err
:= Get(ts
.URL
+ path
)
570 b
, err
:= ioutil
.ReadAll(res
.Body
)
572 t
.Fatal("reading Body:", err
)
574 if s
:= string(b
); s
!= want
{
575 t
.Errorf("for path %q got %q, want %q", path
, s
, want
)
581 func TestFileServerZeroByte(t
*testing
.T
) {
583 ts
:= httptest
.NewServer(FileServer(Dir(".")))
586 res
, err
:= Get(ts
.URL
+ "/..\x00")
590 b
, err
:= ioutil
.ReadAll(res
.Body
)
592 t
.Fatal("reading Body:", err
)
594 if res
.StatusCode
== 200 {
595 t
.Errorf("got status 200; want an error. Body is:\n%s", string(b
))
599 type fakeFileInfo
struct {
608 func (f
*fakeFileInfo
) Name() string { return f
.basename
}
609 func (f
*fakeFileInfo
) Sys() interface{} { return nil }
610 func (f
*fakeFileInfo
) ModTime() time
.Time
{ return f
.modtime
}
611 func (f
*fakeFileInfo
) IsDir() bool { return f
.dir
}
612 func (f
*fakeFileInfo
) Size() int64 { return int64(len(f
.contents
)) }
613 func (f
*fakeFileInfo
) Mode() os
.FileMode
{
615 return 0755 | os
.ModeDir
620 type fakeFile
struct {
623 path
string // as opened
627 func (f
*fakeFile
) Close() error
{ return nil }
628 func (f
*fakeFile
) Stat() (os
.FileInfo
, error
) { return f
.fi
, nil }
629 func (f
*fakeFile
) Readdir(count
int) ([]os
.FileInfo
, error
) {
631 return nil, os
.ErrInvalid
633 var fis
[]os
.FileInfo
635 limit
:= f
.entpos
+ count
636 if count
<= 0 || limit
> len(f
.fi
.ents
) {
637 limit
= len(f
.fi
.ents
)
639 for ; f
.entpos
< limit
; f
.entpos
++ {
640 fis
= append(fis
, f
.fi
.ents
[f
.entpos
])
643 if len(fis
) == 0 && count
> 0 {
650 type fakeFS
map[string]*fakeFileInfo
652 func (fs fakeFS
) Open(name
string) (File
, error
) {
653 name
= path
.Clean(name
)
656 return nil, os
.ErrNotExist
661 return &fakeFile
{ReadSeeker
: strings
.NewReader(f
.contents
), fi
: f
, path
: name
}, nil
664 func TestDirectoryIfNotModified(t
*testing
.T
) {
666 const indexContents
= "I am a fake index.html file"
667 fileMod
:= time
.Unix(1000000000, 0).UTC()
668 fileModStr
:= fileMod
.Format(TimeFormat
)
669 dirMod
:= time
.Unix(123, 0).UTC()
670 indexFile
:= &fakeFileInfo
{
671 basename
: "index.html",
673 contents
: indexContents
,
679 ents
: []*fakeFileInfo
{indexFile
},
681 "/index.html": indexFile
,
684 ts
:= httptest
.NewServer(FileServer(fs
))
687 res
, err
:= Get(ts
.URL
)
691 b
, err
:= ioutil
.ReadAll(res
.Body
)
695 if string(b
) != indexContents
{
696 t
.Fatalf("Got body %q; want %q", b
, indexContents
)
700 lastMod
:= res
.Header
.Get("Last-Modified")
701 if lastMod
!= fileModStr
{
702 t
.Fatalf("initial Last-Modified = %q; want %q", lastMod
, fileModStr
)
705 req
, _
:= NewRequest("GET", ts
.URL
, nil)
706 req
.Header
.Set("If-Modified-Since", lastMod
)
713 if res
.StatusCode
!= 304 {
714 t
.Fatalf("Code after If-Modified-Since request = %v; want 304", res
.StatusCode
)
718 // Advance the index.html file's modtime, but not the directory's.
719 indexFile
.modtime
= indexFile
.modtime
.Add(1 * time
.Hour
)
725 if res
.StatusCode
!= 200 {
726 t
.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res
.StatusCode
, res
)
731 func mustStat(t
*testing
.T
, fileName
string) os
.FileInfo
{
732 fi
, err
:= os
.Stat(fileName
)
739 func TestServeContent(t
*testing
.T
) {
741 type serveParam
struct {
744 content io
.ReadSeeker
748 servec
:= make(chan serveParam
, 1)
749 ts
:= httptest
.NewServer(HandlerFunc(func(w ResponseWriter
, r
*Request
) {
752 w
.Header().Set("ETag", p
.etag
)
754 if p
.contentType
!= "" {
755 w
.Header().Set("Content-Type", p
.contentType
)
757 ServeContent(w
, r
, p
.name
, p
.modtime
, p
.content
)
761 type testCase
struct {
762 // One of file or content must be set:
764 content io
.ReadSeeker
767 serveETag
string // optional
768 serveContentType
string // optional
769 reqHeader
map[string]string
771 wantContentType
string
772 wantContentRange
string
775 htmlModTime
:= mustStat(t
, "testdata/index.html").ModTime()
776 tests
:= map[string]testCase
{
777 "no_last_modified": {
778 file
: "testdata/style.css",
779 wantContentType
: "text/css; charset=utf-8",
782 "with_last_modified": {
783 file
: "testdata/index.html",
784 wantContentType
: "text/html; charset=utf-8",
785 modtime
: htmlModTime
,
786 wantLastMod
: htmlModTime
.UTC().Format(TimeFormat
),
789 "not_modified_modtime": {
790 file
: "testdata/style.css",
791 serveETag
: `"foo"`, // Last-Modified sent only when no ETag
792 modtime
: htmlModTime
,
793 reqHeader
: map[string]string{
794 "If-Modified-Since": htmlModTime
.UTC().Format(TimeFormat
),
798 "not_modified_modtime_with_contenttype": {
799 file
: "testdata/style.css",
800 serveContentType
: "text/css", // explicit content type
801 serveETag
: `"foo"`, // Last-Modified sent only when no ETag
802 modtime
: htmlModTime
,
803 reqHeader
: map[string]string{
804 "If-Modified-Since": htmlModTime
.UTC().Format(TimeFormat
),
808 "not_modified_etag": {
809 file
: "testdata/style.css",
811 reqHeader
: map[string]string{
812 "If-None-Match": `"foo"`,
816 "not_modified_etag_no_seek": {
817 content
: panicOnSeek
{nil}, // should never be called
818 serveETag
: `W/"foo"`, // If-None-Match uses weak ETag comparison
819 reqHeader
: map[string]string{
820 "If-None-Match": `"baz", W/"foo"`,
824 "if_none_match_mismatch": {
825 file
: "testdata/style.css",
827 reqHeader
: map[string]string{
828 "If-None-Match": `"Foo"`,
831 wantContentType
: "text/css; charset=utf-8",
834 file
: "testdata/style.css",
836 reqHeader
: map[string]string{
837 "Range": "bytes=0-4",
839 wantStatus
: StatusPartialContent
,
840 wantContentType
: "text/css; charset=utf-8",
841 wantContentRange
: "bytes 0-4/8",
844 file
: "testdata/style.css",
846 reqHeader
: map[string]string{
847 "Range": "bytes=0-4",
850 wantStatus
: StatusPartialContent
,
851 wantContentType
: "text/css; charset=utf-8",
852 wantContentRange
: "bytes 0-4/8",
854 "range_match_weak_etag": {
855 file
: "testdata/style.css",
857 reqHeader
: map[string]string{
858 "Range": "bytes=0-4",
862 wantContentType
: "text/css; charset=utf-8",
864 "range_no_overlap": {
865 file
: "testdata/style.css",
867 reqHeader
: map[string]string{
868 "Range": "bytes=10-20",
870 wantStatus
: StatusRequestedRangeNotSatisfiable
,
871 wantContentType
: "text/plain; charset=utf-8",
872 wantContentRange
: "bytes */8",
874 // An If-Range resource for entity "A", but entity "B" is now current.
875 // The Range request should be ignored.
877 file
: "testdata/style.css",
879 reqHeader
: map[string]string{
880 "Range": "bytes=0-4",
884 wantContentType
: "text/css; charset=utf-8",
886 "range_with_modtime": {
887 file
: "testdata/style.css",
888 modtime
: time
.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time
.UTC
),
889 reqHeader
: map[string]string{
890 "Range": "bytes=0-4",
891 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
893 wantStatus
: StatusPartialContent
,
894 wantContentType
: "text/css; charset=utf-8",
895 wantContentRange
: "bytes 0-4/8",
896 wantLastMod
: "Wed, 25 Jun 2014 17:12:18 GMT",
898 "range_with_modtime_nanos": {
899 file
: "testdata/style.css",
900 modtime
: time
.Date(2014, 6, 25, 17, 12, 18, 123 /* nanos */, time
.UTC
),
901 reqHeader
: map[string]string{
902 "Range": "bytes=0-4",
903 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
905 wantStatus
: StatusPartialContent
,
906 wantContentType
: "text/css; charset=utf-8",
907 wantContentRange
: "bytes 0-4/8",
908 wantLastMod
: "Wed, 25 Jun 2014 17:12:18 GMT",
910 "unix_zero_modtime": {
911 content
: strings
.NewReader("<html>foo"),
912 modtime
: time
.Unix(0, 0),
913 wantStatus
: StatusOK
,
914 wantContentType
: "text/html; charset=utf-8",
917 file
: "testdata/style.css",
919 reqHeader
: map[string]string{
920 "If-Match": `"Z", "A"`,
923 wantContentType
: "text/css; charset=utf-8",
926 file
: "testdata/style.css",
928 reqHeader
: map[string]string{
932 wantContentType
: "text/css; charset=utf-8",
935 file
: "testdata/style.css",
937 reqHeader
: map[string]string{
941 wantContentType
: "text/plain; charset=utf-8",
943 "ifmatch_fails_on_weak_etag": {
944 file
: "testdata/style.css",
946 reqHeader
: map[string]string{
950 wantContentType
: "text/plain; charset=utf-8",
952 "if_unmodified_since_true": {
953 file
: "testdata/style.css",
954 modtime
: htmlModTime
,
955 reqHeader
: map[string]string{
956 "If-Unmodified-Since": htmlModTime
.UTC().Format(TimeFormat
),
959 wantContentType
: "text/css; charset=utf-8",
960 wantLastMod
: htmlModTime
.UTC().Format(TimeFormat
),
962 "if_unmodified_since_false": {
963 file
: "testdata/style.css",
964 modtime
: htmlModTime
,
965 reqHeader
: map[string]string{
966 "If-Unmodified-Since": htmlModTime
.Add(-2 * time
.Second
).UTC().Format(TimeFormat
),
969 wantContentType
: "text/plain; charset=utf-8",
970 wantLastMod
: htmlModTime
.UTC().Format(TimeFormat
),
973 for testName
, tt
:= range tests
{
974 var content io
.ReadSeeker
976 f
, err
:= os
.Open(tt
.file
)
978 t
.Fatalf("test %q: %v", testName
, err
)
986 servec
<- serveParam
{
987 name
: filepath
.Base(tt
.file
),
991 contentType
: tt
.serveContentType
,
993 req
, err
:= NewRequest("GET", ts
.URL
, nil)
997 for k
, v
:= range tt
.reqHeader
{
1002 res
, err
:= c
.Do(req
)
1006 io
.Copy(ioutil
.Discard
, res
.Body
)
1008 if res
.StatusCode
!= tt
.wantStatus
{
1009 t
.Errorf("test %q: status = %d; want %d", testName
, res
.StatusCode
, tt
.wantStatus
)
1011 if g
, e
:= res
.Header
.Get("Content-Type"), tt
.wantContentType
; g
!= e
{
1012 t
.Errorf("test %q: content-type = %q, want %q", testName
, g
, e
)
1014 if g
, e
:= res
.Header
.Get("Content-Range"), tt
.wantContentRange
; g
!= e
{
1015 t
.Errorf("test %q: content-range = %q, want %q", testName
, g
, e
)
1017 if g
, e
:= res
.Header
.Get("Last-Modified"), tt
.wantLastMod
; g
!= e
{
1018 t
.Errorf("test %q: last-modified = %q, want %q", testName
, g
, e
)
1024 func TestServerFileStatError(t
*testing
.T
) {
1025 rec
:= httptest
.NewRecorder()
1026 r
, _
:= NewRequest("GET", "http://foo/", nil)
1029 fs
:= issue12991FS
{}
1030 ExportServeFile(rec
, r
, fs
, name
, redirect
)
1031 if body
:= rec
.Body
.String(); !strings
.Contains(body
, "403") ||
!strings
.Contains(body
, "Forbidden") {
1032 t
.Errorf("wanted 403 forbidden message; got: %s", body
)
1036 type issue12991FS
struct{}
1038 func (issue12991FS
) Open(string) (File
, error
) { return issue12991File
{}, nil }
1040 type issue12991File
struct{ File
}
1042 func (issue12991File
) Stat() (os
.FileInfo
, error
) { return nil, os
.ErrPermission
}
1043 func (issue12991File
) Close() error
{ return nil }
1045 func TestServeContentErrorMessages(t
*testing
.T
) {
1048 "/500": &fakeFileInfo
{
1049 err
: errors
.New("random error"),
1051 "/403": &fakeFileInfo
{
1052 err
: &os
.PathError
{Err
: os
.ErrPermission
},
1055 ts
:= httptest
.NewServer(FileServer(fs
))
1058 for _
, code
:= range []int{403, 404, 500} {
1059 res
, err
:= c
.Get(fmt
.Sprintf("%s/%d", ts
.URL
, code
))
1061 t
.Errorf("Error fetching /%d: %v", code
, err
)
1064 if res
.StatusCode
!= code
{
1065 t
.Errorf("For /%d, status code = %d; want %d", code
, res
.StatusCode
, code
)
1071 // verifies that sendfile is being used on Linux
1072 func TestLinuxSendfile(t
*testing
.T
) {
1075 if runtime
.GOOS
!= "linux" {
1076 t
.Skip("skipping; linux-only test")
1078 if _
, err
:= exec
.LookPath("strace"); err
!= nil {
1079 t
.Skip("skipping; strace not found in path")
1082 ln
, err
:= net
.Listen("tcp", "127.0.0.1:0")
1086 lnf
, err
:= ln
.(*net
.TCPListener
).File()
1092 syscalls
:= "sendfile,sendfile64"
1093 switch runtime
.GOARCH
{
1094 case "mips64", "mips64le", "s390x", "alpha":
1095 // strace on the above platforms doesn't support sendfile64
1096 // and will error out if we specify that with `-e trace='.
1097 syscalls
= "sendfile"
1100 // Attempt to run strace, and skip on failure - this test requires SYS_PTRACE.
1101 if err
:= exec
.Command("strace", "-f", "-q", "-e", "trace="+syscalls
, os
.Args
[0], "-test.run=^$").Run(); err
!= nil {
1102 t
.Skipf("skipping; failed to run strace: %v", err
)
1105 var buf bytes
.Buffer
1106 child
:= exec
.Command("strace", "-f", "-q", "-e", "trace="+syscalls
, os
.Args
[0], "-test.run=TestLinuxSendfileChild")
1107 child
.ExtraFiles
= append(child
.ExtraFiles
, lnf
)
1108 child
.Env
= append([]string{"GO_WANT_HELPER_PROCESS=1"}, os
.Environ()...)
1111 if err
:= child
.Start(); err
!= nil {
1112 t
.Skipf("skipping; failed to start straced child: %v", err
)
1115 res
, err
:= Get(fmt
.Sprintf("http://%s/", ln
.Addr()))
1117 t
.Fatalf("http client error: %v", err
)
1119 _
, err
= io
.Copy(ioutil
.Discard
, res
.Body
)
1121 t
.Fatalf("client body read error: %v", err
)
1125 // Force child to exit cleanly.
1126 Post(fmt
.Sprintf("http://%s/quit", ln
.Addr()), "", nil)
1129 rx
:= regexp
.MustCompile(`sendfile(64)?\(\d+,\s*\d+,\s*NULL,\s*\d+`)
1131 if !rx
.MatchString(out
) {
1132 t
.Errorf("no sendfile system call found in:\n%s", out
)
1136 func getBody(t
*testing
.T
, testName
string, req Request
, client
*Client
) (*Response
, []byte) {
1137 r
, err
:= client
.Do(&req
)
1139 t
.Fatalf("%s: for URL %q, send error: %v", testName
, req
.URL
.String(), err
)
1141 b
, err
:= ioutil
.ReadAll(r
.Body
)
1143 t
.Fatalf("%s: for URL %q, reading body: %v", testName
, req
.URL
.String(), err
)
1148 // TestLinuxSendfileChild isn't a real test. It's used as a helper process
1149 // for TestLinuxSendfile.
1150 func TestLinuxSendfileChild(*testing
.T
) {
1151 if os
.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
1155 fd3
:= os
.NewFile(3, "ephemeral-port-listener")
1156 ln
, err
:= net
.FileListener(fd3
)
1160 mux
:= NewServeMux()
1161 mux
.Handle("/", FileServer(Dir("testdata")))
1162 mux
.HandleFunc("/quit", func(ResponseWriter
, *Request
) {
1165 s
:= &Server
{Handler
: mux
}
1172 // Issue 18984: tests that requests for paths beyond files return not-found errors
1173 func TestFileServerNotDirError(t
*testing
.T
) {
1175 ts
:= httptest
.NewServer(FileServer(Dir("testdata")))
1178 res
, err
:= Get(ts
.URL
+ "/index.html/not-a-file")
1183 if res
.StatusCode
!= 404 {
1184 t
.Errorf("StatusCode = %v; want 404", res
.StatusCode
)
1187 test
:= func(name
string, dir Dir
) {
1188 t
.Run(name
, func(t
*testing
.T
) {
1189 _
, err
= dir
.Open("/index.html/not-a-file")
1191 t
.Fatal("err == nil; want != nil")
1193 if !os
.IsNotExist(err
) {
1194 t
.Errorf("err = %v; os.IsNotExist(err) = %v; want true", err
, os
.IsNotExist(err
))
1197 _
, err
= dir
.Open("/index.html/not-a-dir/not-a-file")
1199 t
.Fatal("err == nil; want != nil")
1201 if !os
.IsNotExist(err
) {
1202 t
.Errorf("err = %v; os.IsNotExist(err) = %v; want true", err
, os
.IsNotExist(err
))
1207 absPath
, err
:= filepath
.Abs("testdata")
1209 t
.Fatal("get abs path:", err
)
1212 test("RelativePath", Dir("testdata"))
1213 test("AbsolutePath", Dir(absPath
))
1216 func TestFileServerCleanPath(t
*testing
.T
) {
1222 {"/", 200, []string{"/", "/index.html"}},
1223 {"/dir", 301, []string{"/dir"}},
1224 {"/dir/", 200, []string{"/dir", "/dir/index.html"}},
1226 for _
, tt
:= range tests
{
1228 rr
:= httptest
.NewRecorder()
1229 req
, _
:= NewRequest("GET", "http://foo.localhost"+tt
.path
, nil)
1230 FileServer(fileServerCleanPathDir
{&log
}).ServeHTTP(rr
, req
)
1231 if !reflect
.DeepEqual(log
, tt
.wantOpen
) {
1232 t
.Logf("For %s: Opens = %q; want %q", tt
.path
, log
, tt
.wantOpen
)
1234 if rr
.Code
!= tt
.wantCode
{
1235 t
.Logf("For %s: Response code = %d; want %d", tt
.path
, rr
.Code
, tt
.wantCode
)
1240 type fileServerCleanPathDir
struct {
1244 func (d fileServerCleanPathDir
) Open(path
string) (File
, error
) {
1245 *(d
.log
) = append(*(d
.log
), path
)
1246 if path
== "/" || path
== "/dir" || path
== "/dir/" {
1247 // Just return back something that's a directory.
1248 return Dir(".").Open(".")
1250 return nil, os
.ErrNotExist
1253 type panicOnSeek
struct{ io
.ReadSeeker
}
1255 func Test_scanETag(t
*testing
.T
) {
1261 {`W/"etag-1"`, `W/"etag-1"`, ""},
1262 {`"etag-2"`, `"etag-2"`, ""},
1263 {`"etag-1", "etag-2"`, `"etag-1"`, `, "etag-2"`},
1266 {`W/"truc`, "", ""},
1267 {`w/"case-sensitive"`, "", ""},
1268 {`"spaced etag"`, "", ""},
1270 for _
, test
:= range tests
{
1271 etag
, remain
:= ExportScanETag(test
.in
)
1272 if etag
!= test
.wantETag || remain
!= test
.wantRemain
{
1273 t
.Errorf("scanETag(%q)=%q %q, want %q %q", test
.in
, etag
, remain
, test
.wantETag
, test
.wantRemain
)