]> Cypherpunks.ru repositories - gostls13.git/blob - src/net/http/fs_test.go
net/http: set/override Content-Length for encoded range requests
[gostls13.git] / src / net / http / fs_test.go
1 // Copyright 2010 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4
5 package http_test
6
7 import (
8         "bufio"
9         "bytes"
10         "errors"
11         "fmt"
12         "internal/testenv"
13         "io"
14         "io/fs"
15         "mime"
16         "mime/multipart"
17         "net"
18         . "net/http"
19         "net/http/httptest"
20         "net/url"
21         "os"
22         "os/exec"
23         "path"
24         "path/filepath"
25         "reflect"
26         "regexp"
27         "runtime"
28         "strings"
29         "testing"
30         "testing/fstest"
31         "time"
32 )
33
34 const (
35         testFile    = "testdata/file"
36         testFileLen = 11
37 )
38
39 type wantRange struct {
40         start, end int64 // range [start,end)
41 }
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=0-0,-2", code: StatusPartialContent, ranges: []wantRange{{0, 1}, {testFileLen - 2, testFileLen}}},
54         {r: "bytes=0-1,5-8", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, 9}}},
55         {r: "bytes=0-1,5-", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, testFileLen}}},
56         {r: "bytes=5-1000", code: StatusPartialContent, ranges: []wantRange{{5, testFileLen}}},
57         {r: "bytes=0-,1-,2-,3-,4-", code: StatusOK}, // ignore wasteful range request
58         {r: "bytes=0-9", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen - 1}}},
59         {r: "bytes=0-10", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
60         {r: "bytes=0-11", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
61         {r: "bytes=10-11", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
62         {r: "bytes=10-", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
63         {r: "bytes=11-", code: StatusRequestedRangeNotSatisfiable},
64         {r: "bytes=11-12", code: StatusRequestedRangeNotSatisfiable},
65         {r: "bytes=12-12", code: StatusRequestedRangeNotSatisfiable},
66         {r: "bytes=11-100", code: StatusRequestedRangeNotSatisfiable},
67         {r: "bytes=12-100", code: StatusRequestedRangeNotSatisfiable},
68         {r: "bytes=100-", code: StatusRequestedRangeNotSatisfiable},
69         {r: "bytes=100-1000", code: StatusRequestedRangeNotSatisfiable},
70 }
71
72 func TestServeFile(t *testing.T) { run(t, testServeFile) }
73 func testServeFile(t *testing.T, mode testMode) {
74         ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
75                 ServeFile(w, r, "testdata/file")
76         })).ts
77         c := ts.Client()
78
79         var err error
80
81         file, err := os.ReadFile(testFile)
82         if err != nil {
83                 t.Fatal("reading file:", err)
84         }
85
86         // set up the Request (re-used for all tests)
87         var req Request
88         req.Header = make(Header)
89         if req.URL, err = url.Parse(ts.URL); err != nil {
90                 t.Fatal("ParseURL:", err)
91         }
92
93         // Get contents via various methods.
94         //
95         // See https://go.dev/issue/59471 for a proposal to limit the set of methods handled.
96         // For now, test the historical behavior.
97         for _, method := range []string{
98                 MethodGet,
99                 MethodPost,
100                 MethodPut,
101                 MethodPatch,
102                 MethodDelete,
103                 MethodOptions,
104                 MethodTrace,
105         } {
106                 req.Method = method
107                 _, body := getBody(t, method, req, c)
108                 if !bytes.Equal(body, file) {
109                         t.Fatalf("body mismatch for %v request: got %q, want %q", method, body, file)
110                 }
111         }
112
113         // HEAD request.
114         req.Method = MethodHead
115         resp, body := getBody(t, "HEAD", req, c)
116         if len(body) != 0 {
117                 t.Fatalf("body mismatch for HEAD request: got %q, want empty", body)
118         }
119         if got, want := resp.Header.Get("Content-Length"), fmt.Sprint(len(file)); got != want {
120                 t.Fatalf("Content-Length mismatch for HEAD request: got %v, want %v", got, want)
121         }
122
123         // Range tests
124         req.Method = MethodGet
125 Cases:
126         for _, rt := range ServeFileRangeTests {
127                 if rt.r != "" {
128                         req.Header.Set("Range", rt.r)
129                 }
130                 resp, body := getBody(t, fmt.Sprintf("range test %q", rt.r), req, c)
131                 if resp.StatusCode != rt.code {
132                         t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, resp.StatusCode, rt.code)
133                 }
134                 if rt.code == StatusRequestedRangeNotSatisfiable {
135                         continue
136                 }
137                 wantContentRange := ""
138                 if len(rt.ranges) == 1 {
139                         rng := rt.ranges[0]
140                         wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
141                 }
142                 cr := resp.Header.Get("Content-Range")
143                 if cr != wantContentRange {
144                         t.Errorf("range=%q: Content-Range = %q, want %q", rt.r, cr, wantContentRange)
145                 }
146                 ct := resp.Header.Get("Content-Type")
147                 if len(rt.ranges) == 1 {
148                         rng := rt.ranges[0]
149                         wantBody := file[rng.start:rng.end]
150                         if !bytes.Equal(body, wantBody) {
151                                 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
152                         }
153                         if strings.HasPrefix(ct, "multipart/byteranges") {
154                                 t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct)
155                         }
156                 }
157                 if len(rt.ranges) > 1 {
158                         typ, params, err := mime.ParseMediaType(ct)
159                         if err != nil {
160                                 t.Errorf("range=%q content-type = %q; %v", rt.r, ct, err)
161                                 continue
162                         }
163                         if typ != "multipart/byteranges" {
164                                 t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ)
165                                 continue
166                         }
167                         if params["boundary"] == "" {
168                                 t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct)
169                                 continue
170                         }
171                         if g, w := resp.ContentLength, int64(len(body)); g != w {
172                                 t.Errorf("range=%q Content-Length = %d; want %d", rt.r, g, w)
173                                 continue
174                         }
175                         mr := multipart.NewReader(bytes.NewReader(body), params["boundary"])
176                         for ri, rng := range rt.ranges {
177                                 part, err := mr.NextPart()
178                                 if err != nil {
179                                         t.Errorf("range=%q, reading part index %d: %v", rt.r, ri, err)
180                                         continue Cases
181                                 }
182                                 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
183                                 if g, w := part.Header.Get("Content-Range"), wantContentRange; g != w {
184                                         t.Errorf("range=%q: part Content-Range = %q; want %q", rt.r, g, w)
185                                 }
186                                 body, err := io.ReadAll(part)
187                                 if err != nil {
188                                         t.Errorf("range=%q, reading part index %d body: %v", rt.r, ri, err)
189                                         continue Cases
190                                 }
191                                 wantBody := file[rng.start:rng.end]
192                                 if !bytes.Equal(body, wantBody) {
193                                         t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
194                                 }
195                         }
196                         _, err = mr.NextPart()
197                         if err != io.EOF {
198                                 t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err)
199                         }
200                 }
201         }
202 }
203
204 func TestServeFile_DotDot(t *testing.T) {
205         tests := []struct {
206                 req        string
207                 wantStatus int
208         }{
209                 {"/testdata/file", 200},
210                 {"/../file", 400},
211                 {"/..", 400},
212                 {"/../", 400},
213                 {"/../foo", 400},
214                 {"/..\\foo", 400},
215                 {"/file/a", 200},
216                 {"/file/a..", 200},
217                 {"/file/a/..", 400},
218                 {"/file/a\\..", 400},
219         }
220         for _, tt := range tests {
221                 req, err := ReadRequest(bufio.NewReader(strings.NewReader("GET " + tt.req + " HTTP/1.1\r\nHost: foo\r\n\r\n")))
222                 if err != nil {
223                         t.Errorf("bad request %q: %v", tt.req, err)
224                         continue
225                 }
226                 rec := httptest.NewRecorder()
227                 ServeFile(rec, req, "testdata/file")
228                 if rec.Code != tt.wantStatus {
229                         t.Errorf("for request %q, status = %d; want %d", tt.req, rec.Code, tt.wantStatus)
230                 }
231         }
232 }
233
234 // Tests that this doesn't panic. (Issue 30165)
235 func TestServeFileDirPanicEmptyPath(t *testing.T) {
236         rec := httptest.NewRecorder()
237         req := httptest.NewRequest("GET", "/", nil)
238         req.URL.Path = ""
239         ServeFile(rec, req, "testdata")
240         res := rec.Result()
241         if res.StatusCode != 301 {
242                 t.Errorf("code = %v; want 301", res.Status)
243         }
244 }
245
246 // Tests that ranges are ignored with serving empty content. (Issue 54794)
247 func TestServeContentWithEmptyContentIgnoreRanges(t *testing.T) {
248         for _, r := range []string{
249                 "bytes=0-128",
250                 "bytes=1-",
251         } {
252                 rec := httptest.NewRecorder()
253                 req := httptest.NewRequest("GET", "/", nil)
254                 req.Header.Set("Range", r)
255                 ServeContent(rec, req, "nothing", time.Now(), bytes.NewReader(nil))
256                 res := rec.Result()
257                 if res.StatusCode != 200 {
258                         t.Errorf("code = %v; want 200", res.Status)
259                 }
260                 bodyLen := rec.Body.Len()
261                 if bodyLen != 0 {
262                         t.Errorf("body.Len() = %v; want 0", res.Status)
263                 }
264         }
265 }
266
267 var fsRedirectTestData = []struct {
268         original, redirect string
269 }{
270         {"/test/index.html", "/test/"},
271         {"/test/testdata", "/test/testdata/"},
272         {"/test/testdata/file/", "/test/testdata/file"},
273 }
274
275 func TestFSRedirect(t *testing.T) { run(t, testFSRedirect) }
276 func testFSRedirect(t *testing.T, mode testMode) {
277         ts := newClientServerTest(t, mode, StripPrefix("/test", FileServer(Dir(".")))).ts
278
279         for _, data := range fsRedirectTestData {
280                 res, err := ts.Client().Get(ts.URL + data.original)
281                 if err != nil {
282                         t.Fatal(err)
283                 }
284                 res.Body.Close()
285                 if g, e := res.Request.URL.Path, data.redirect; g != e {
286                         t.Errorf("redirect from %s: got %s, want %s", data.original, g, e)
287                 }
288         }
289 }
290
291 type testFileSystem struct {
292         open func(name string) (File, error)
293 }
294
295 func (fs *testFileSystem) Open(name string) (File, error) {
296         return fs.open(name)
297 }
298
299 func TestFileServerCleans(t *testing.T) {
300         defer afterTest(t)
301         ch := make(chan string, 1)
302         fs := FileServer(&testFileSystem{func(name string) (File, error) {
303                 ch <- name
304                 return nil, errors.New("file does not exist")
305         }})
306         tests := []struct {
307                 reqPath, openArg string
308         }{
309                 {"/foo.txt", "/foo.txt"},
310                 {"//foo.txt", "/foo.txt"},
311                 {"/../foo.txt", "/foo.txt"},
312         }
313         req, _ := NewRequest("GET", "http://example.com", nil)
314         for n, test := range tests {
315                 rec := httptest.NewRecorder()
316                 req.URL.Path = test.reqPath
317                 fs.ServeHTTP(rec, req)
318                 if got := <-ch; got != test.openArg {
319                         t.Errorf("test %d: got %q, want %q", n, got, test.openArg)
320                 }
321         }
322 }
323
324 func TestFileServerEscapesNames(t *testing.T) { run(t, testFileServerEscapesNames) }
325 func testFileServerEscapesNames(t *testing.T, mode testMode) {
326         const dirListPrefix = "<pre>\n"
327         const dirListSuffix = "\n</pre>\n"
328         tests := []struct {
329                 name, escaped string
330         }{
331                 {`simple_name`, `<a href="simple_name">simple_name</a>`},
332                 {`"'<>&`, `<a href="%22%27%3C%3E&">&#34;&#39;&lt;&gt;&amp;</a>`},
333                 {`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`},
334                 {`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo">&lt;combo&gt;?foo</a>`},
335                 {`foo:bar`, `<a href="./foo:bar">foo:bar</a>`},
336         }
337
338         // We put each test file in its own directory in the fakeFS so we can look at it in isolation.
339         fs := make(fakeFS)
340         for i, test := range tests {
341                 testFile := &fakeFileInfo{basename: test.name}
342                 fs[fmt.Sprintf("/%d", i)] = &fakeFileInfo{
343                         dir:     true,
344                         modtime: time.Unix(1000000000, 0).UTC(),
345                         ents:    []*fakeFileInfo{testFile},
346                 }
347                 fs[fmt.Sprintf("/%d/%s", i, test.name)] = testFile
348         }
349
350         ts := newClientServerTest(t, mode, FileServer(&fs)).ts
351         for i, test := range tests {
352                 url := fmt.Sprintf("%s/%d", ts.URL, i)
353                 res, err := ts.Client().Get(url)
354                 if err != nil {
355                         t.Fatalf("test %q: Get: %v", test.name, err)
356                 }
357                 b, err := io.ReadAll(res.Body)
358                 if err != nil {
359                         t.Fatalf("test %q: read Body: %v", test.name, err)
360                 }
361                 s := string(b)
362                 if !strings.HasPrefix(s, dirListPrefix) || !strings.HasSuffix(s, dirListSuffix) {
363                         t.Errorf("test %q: listing dir, full output is %q, want prefix %q and suffix %q", test.name, s, dirListPrefix, dirListSuffix)
364                 }
365                 if trimmed := strings.TrimSuffix(strings.TrimPrefix(s, dirListPrefix), dirListSuffix); trimmed != test.escaped {
366                         t.Errorf("test %q: listing dir, filename escaped to %q, want %q", test.name, trimmed, test.escaped)
367                 }
368                 res.Body.Close()
369         }
370 }
371
372 func TestFileServerSortsNames(t *testing.T) { run(t, testFileServerSortsNames) }
373 func testFileServerSortsNames(t *testing.T, mode testMode) {
374         const contents = "I am a fake file"
375         dirMod := time.Unix(123, 0).UTC()
376         fileMod := time.Unix(1000000000, 0).UTC()
377         fs := fakeFS{
378                 "/": &fakeFileInfo{
379                         dir:     true,
380                         modtime: dirMod,
381                         ents: []*fakeFileInfo{
382                                 {
383                                         basename: "b",
384                                         modtime:  fileMod,
385                                         contents: contents,
386                                 },
387                                 {
388                                         basename: "a",
389                                         modtime:  fileMod,
390                                         contents: contents,
391                                 },
392                         },
393                 },
394         }
395
396         ts := newClientServerTest(t, mode, FileServer(&fs)).ts
397
398         res, err := ts.Client().Get(ts.URL)
399         if err != nil {
400                 t.Fatalf("Get: %v", err)
401         }
402         defer res.Body.Close()
403
404         b, err := io.ReadAll(res.Body)
405         if err != nil {
406                 t.Fatalf("read Body: %v", err)
407         }
408         s := string(b)
409         if !strings.Contains(s, "<a href=\"a\">a</a>\n<a href=\"b\">b</a>") {
410                 t.Errorf("output appears to be unsorted:\n%s", s)
411         }
412 }
413
414 func mustRemoveAll(dir string) {
415         err := os.RemoveAll(dir)
416         if err != nil {
417                 panic(err)
418         }
419 }
420
421 func TestFileServerImplicitLeadingSlash(t *testing.T) { run(t, testFileServerImplicitLeadingSlash) }
422 func testFileServerImplicitLeadingSlash(t *testing.T, mode testMode) {
423         tempDir := t.TempDir()
424         if err := os.WriteFile(filepath.Join(tempDir, "foo.txt"), []byte("Hello world"), 0644); err != nil {
425                 t.Fatalf("WriteFile: %v", err)
426         }
427         ts := newClientServerTest(t, mode, StripPrefix("/bar/", FileServer(Dir(tempDir)))).ts
428         get := func(suffix string) string {
429                 res, err := ts.Client().Get(ts.URL + suffix)
430                 if err != nil {
431                         t.Fatalf("Get %s: %v", suffix, err)
432                 }
433                 b, err := io.ReadAll(res.Body)
434                 if err != nil {
435                         t.Fatalf("ReadAll %s: %v", suffix, err)
436                 }
437                 res.Body.Close()
438                 return string(b)
439         }
440         if s := get("/bar/"); !strings.Contains(s, ">foo.txt<") {
441                 t.Logf("expected a directory listing with foo.txt, got %q", s)
442         }
443         if s := get("/bar/foo.txt"); s != "Hello world" {
444                 t.Logf("expected %q, got %q", "Hello world", s)
445         }
446 }
447
448 func TestDirJoin(t *testing.T) {
449         if runtime.GOOS == "windows" {
450                 t.Skip("skipping test on windows")
451         }
452         wfi, err := os.Stat("/etc/hosts")
453         if err != nil {
454                 t.Skip("skipping test; no /etc/hosts file")
455         }
456         test := func(d Dir, name string) {
457                 f, err := d.Open(name)
458                 if err != nil {
459                         t.Fatalf("open of %s: %v", name, err)
460                 }
461                 defer f.Close()
462                 gfi, err := f.Stat()
463                 if err != nil {
464                         t.Fatalf("stat of %s: %v", name, err)
465                 }
466                 if !os.SameFile(gfi, wfi) {
467                         t.Errorf("%s got different file", name)
468                 }
469         }
470         test(Dir("/etc/"), "/hosts")
471         test(Dir("/etc/"), "hosts")
472         test(Dir("/etc/"), "../../../../hosts")
473         test(Dir("/etc"), "/hosts")
474         test(Dir("/etc"), "hosts")
475         test(Dir("/etc"), "../../../../hosts")
476
477         // Not really directories, but since we use this trick in
478         // ServeFile, test it:
479         test(Dir("/etc/hosts"), "")
480         test(Dir("/etc/hosts"), "/")
481         test(Dir("/etc/hosts"), "../")
482 }
483
484 func TestEmptyDirOpenCWD(t *testing.T) {
485         test := func(d Dir) {
486                 name := "fs_test.go"
487                 f, err := d.Open(name)
488                 if err != nil {
489                         t.Fatalf("open of %s: %v", name, err)
490                 }
491                 defer f.Close()
492         }
493         test(Dir(""))
494         test(Dir("."))
495         test(Dir("./"))
496 }
497
498 func TestServeFileContentType(t *testing.T) { run(t, testServeFileContentType) }
499 func testServeFileContentType(t *testing.T, mode testMode) {
500         const ctype = "icecream/chocolate"
501         ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
502                 switch r.FormValue("override") {
503                 case "1":
504                         w.Header().Set("Content-Type", ctype)
505                 case "2":
506                         // Explicitly inhibit sniffing.
507                         w.Header()["Content-Type"] = []string{}
508                 }
509                 ServeFile(w, r, "testdata/file")
510         })).ts
511         get := func(override string, want []string) {
512                 resp, err := ts.Client().Get(ts.URL + "?override=" + override)
513                 if err != nil {
514                         t.Fatal(err)
515                 }
516                 if h := resp.Header["Content-Type"]; !reflect.DeepEqual(h, want) {
517                         t.Errorf("Content-Type mismatch: got %v, want %v", h, want)
518                 }
519                 resp.Body.Close()
520         }
521         get("0", []string{"text/plain; charset=utf-8"})
522         get("1", []string{ctype})
523         get("2", nil)
524 }
525
526 func TestServeFileMimeType(t *testing.T) { run(t, testServeFileMimeType) }
527 func testServeFileMimeType(t *testing.T, mode testMode) {
528         ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
529                 ServeFile(w, r, "testdata/style.css")
530         })).ts
531         resp, err := ts.Client().Get(ts.URL)
532         if err != nil {
533                 t.Fatal(err)
534         }
535         resp.Body.Close()
536         want := "text/css; charset=utf-8"
537         if h := resp.Header.Get("Content-Type"); h != want {
538                 t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
539         }
540 }
541
542 func TestServeFileFromCWD(t *testing.T) { run(t, testServeFileFromCWD) }
543 func testServeFileFromCWD(t *testing.T, mode testMode) {
544         ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
545                 ServeFile(w, r, "fs_test.go")
546         })).ts
547         r, err := ts.Client().Get(ts.URL)
548         if err != nil {
549                 t.Fatal(err)
550         }
551         r.Body.Close()
552         if r.StatusCode != 200 {
553                 t.Fatalf("expected 200 OK, got %s", r.Status)
554         }
555 }
556
557 // Issue 13996
558 func TestServeDirWithoutTrailingSlash(t *testing.T) { run(t, testServeDirWithoutTrailingSlash) }
559 func testServeDirWithoutTrailingSlash(t *testing.T, mode testMode) {
560         e := "/testdata/"
561         ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
562                 ServeFile(w, r, ".")
563         })).ts
564         r, err := ts.Client().Get(ts.URL + "/testdata")
565         if err != nil {
566                 t.Fatal(err)
567         }
568         r.Body.Close()
569         if g := r.Request.URL.Path; g != e {
570                 t.Errorf("got %s, want %s", g, e)
571         }
572 }
573
574 // Tests that ServeFile adds a Content-Length even if a Content-Encoding is
575 // specified.
576 func TestServeFileWithContentEncoding(t *testing.T) { run(t, testServeFileWithContentEncoding) }
577 func testServeFileWithContentEncoding(t *testing.T, mode testMode) {
578         cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
579                 w.Header().Set("Content-Encoding", "foo")
580                 ServeFile(w, r, "testdata/file")
581
582                 // Because the testdata is so small, it would fit in
583                 // both the h1 and h2 Server's write buffers. For h1,
584                 // sendfile is used, though, forcing a header flush at
585                 // the io.Copy. http2 doesn't do a header flush so
586                 // buffers all 11 bytes and then adds its own
587                 // Content-Length. To prevent the Server's
588                 // Content-Length and test ServeFile only, flush here.
589                 w.(Flusher).Flush()
590         }))
591         resp, err := cst.c.Get(cst.ts.URL)
592         if err != nil {
593                 t.Fatal(err)
594         }
595         resp.Body.Close()
596         if g, e := resp.ContentLength, int64(11); g != e {
597                 t.Errorf("Content-Length mismatch: got %d, want %d", g, e)
598         }
599 }
600
601 // Tests that ServeFile does not generate representation metadata when
602 // file has not been modified, as per RFC 7232 section 4.1.
603 func TestServeFileNotModified(t *testing.T) { run(t, testServeFileNotModified) }
604 func testServeFileNotModified(t *testing.T, mode testMode) {
605         cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
606                 w.Header().Set("Content-Type", "application/json")
607                 w.Header().Set("Content-Encoding", "foo")
608                 w.Header().Set("Etag", `"123"`)
609                 ServeFile(w, r, "testdata/file")
610
611                 // Because the testdata is so small, it would fit in
612                 // both the h1 and h2 Server's write buffers. For h1,
613                 // sendfile is used, though, forcing a header flush at
614                 // the io.Copy. http2 doesn't do a header flush so
615                 // buffers all 11 bytes and then adds its own
616                 // Content-Length. To prevent the Server's
617                 // Content-Length and test ServeFile only, flush here.
618                 w.(Flusher).Flush()
619         }))
620         req, err := NewRequest("GET", cst.ts.URL, nil)
621         if err != nil {
622                 t.Fatal(err)
623         }
624         req.Header.Set("If-None-Match", `"123"`)
625         resp, err := cst.c.Do(req)
626         if err != nil {
627                 t.Fatal(err)
628         }
629         b, err := io.ReadAll(resp.Body)
630         resp.Body.Close()
631         if err != nil {
632                 t.Fatal("reading Body:", err)
633         }
634         if len(b) != 0 {
635                 t.Errorf("non-empty body")
636         }
637         if g, e := resp.StatusCode, StatusNotModified; g != e {
638                 t.Errorf("status mismatch: got %d, want %d", g, e)
639         }
640         // HTTP1 transport sets ContentLength to 0.
641         if g, e1, e2 := resp.ContentLength, int64(-1), int64(0); g != e1 && g != e2 {
642                 t.Errorf("Content-Length mismatch: got %d, want %d or %d", g, e1, e2)
643         }
644         if resp.Header.Get("Content-Type") != "" {
645                 t.Errorf("Content-Type present, but it should not be")
646         }
647         if resp.Header.Get("Content-Encoding") != "" {
648                 t.Errorf("Content-Encoding present, but it should not be")
649         }
650 }
651
652 func TestServeIndexHtml(t *testing.T) { run(t, testServeIndexHtml) }
653 func testServeIndexHtml(t *testing.T, mode testMode) {
654         for i := 0; i < 2; i++ {
655                 var h Handler
656                 var name string
657                 switch i {
658                 case 0:
659                         h = FileServer(Dir("."))
660                         name = "Dir"
661                 case 1:
662                         h = FileServer(FS(os.DirFS(".")))
663                         name = "DirFS"
664                 }
665                 t.Run(name, func(t *testing.T) {
666                         const want = "index.html says hello\n"
667                         ts := newClientServerTest(t, mode, h).ts
668
669                         for _, path := range []string{"/testdata/", "/testdata/index.html"} {
670                                 res, err := ts.Client().Get(ts.URL + path)
671                                 if err != nil {
672                                         t.Fatal(err)
673                                 }
674                                 b, err := io.ReadAll(res.Body)
675                                 if err != nil {
676                                         t.Fatal("reading Body:", err)
677                                 }
678                                 if s := string(b); s != want {
679                                         t.Errorf("for path %q got %q, want %q", path, s, want)
680                                 }
681                                 res.Body.Close()
682                         }
683                 })
684         }
685 }
686
687 func TestServeIndexHtmlFS(t *testing.T) { run(t, testServeIndexHtmlFS) }
688 func testServeIndexHtmlFS(t *testing.T, mode testMode) {
689         const want = "index.html says hello\n"
690         ts := newClientServerTest(t, mode, FileServer(Dir("."))).ts
691         defer ts.Close()
692
693         for _, path := range []string{"/testdata/", "/testdata/index.html"} {
694                 res, err := ts.Client().Get(ts.URL + path)
695                 if err != nil {
696                         t.Fatal(err)
697                 }
698                 b, err := io.ReadAll(res.Body)
699                 if err != nil {
700                         t.Fatal("reading Body:", err)
701                 }
702                 if s := string(b); s != want {
703                         t.Errorf("for path %q got %q, want %q", path, s, want)
704                 }
705                 res.Body.Close()
706         }
707 }
708
709 func TestFileServerZeroByte(t *testing.T) { run(t, testFileServerZeroByte) }
710 func testFileServerZeroByte(t *testing.T, mode testMode) {
711         ts := newClientServerTest(t, mode, FileServer(Dir("."))).ts
712
713         c, err := net.Dial("tcp", ts.Listener.Addr().String())
714         if err != nil {
715                 t.Fatal(err)
716         }
717         defer c.Close()
718         _, err = fmt.Fprintf(c, "GET /..\x00 HTTP/1.0\r\n\r\n")
719         if err != nil {
720                 t.Fatal(err)
721         }
722         var got bytes.Buffer
723         bufr := bufio.NewReader(io.TeeReader(c, &got))
724         res, err := ReadResponse(bufr, nil)
725         if err != nil {
726                 t.Fatal("ReadResponse: ", err)
727         }
728         if res.StatusCode == 200 {
729                 t.Errorf("got status 200; want an error. Body is:\n%s", got.Bytes())
730         }
731 }
732
733 func TestFileServerNamesEscape(t *testing.T) { run(t, testFileServerNamesEscape) }
734 func testFileServerNamesEscape(t *testing.T, mode testMode) {
735         ts := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts
736         for _, path := range []string{
737                 "/../testdata/file",
738                 "/NUL", // don't read from device files on Windows
739         } {
740                 res, err := ts.Client().Get(ts.URL + path)
741                 if err != nil {
742                         t.Fatal(err)
743                 }
744                 res.Body.Close()
745                 if res.StatusCode < 400 || res.StatusCode > 599 {
746                         t.Errorf("Get(%q): got status %v, want 4xx or 5xx", path, res.StatusCode)
747                 }
748
749         }
750 }
751
752 type fakeFileInfo struct {
753         dir      bool
754         basename string
755         modtime  time.Time
756         ents     []*fakeFileInfo
757         contents string
758         err      error
759 }
760
761 func (f *fakeFileInfo) Name() string       { return f.basename }
762 func (f *fakeFileInfo) Sys() any           { return nil }
763 func (f *fakeFileInfo) ModTime() time.Time { return f.modtime }
764 func (f *fakeFileInfo) IsDir() bool        { return f.dir }
765 func (f *fakeFileInfo) Size() int64        { return int64(len(f.contents)) }
766 func (f *fakeFileInfo) Mode() fs.FileMode {
767         if f.dir {
768                 return 0755 | fs.ModeDir
769         }
770         return 0644
771 }
772
773 func (f *fakeFileInfo) String() string {
774         return fs.FormatFileInfo(f)
775 }
776
777 type fakeFile struct {
778         io.ReadSeeker
779         fi     *fakeFileInfo
780         path   string // as opened
781         entpos int
782 }
783
784 func (f *fakeFile) Close() error               { return nil }
785 func (f *fakeFile) Stat() (fs.FileInfo, error) { return f.fi, nil }
786 func (f *fakeFile) Readdir(count int) ([]fs.FileInfo, error) {
787         if !f.fi.dir {
788                 return nil, fs.ErrInvalid
789         }
790         var fis []fs.FileInfo
791
792         limit := f.entpos + count
793         if count <= 0 || limit > len(f.fi.ents) {
794                 limit = len(f.fi.ents)
795         }
796         for ; f.entpos < limit; f.entpos++ {
797                 fis = append(fis, f.fi.ents[f.entpos])
798         }
799
800         if len(fis) == 0 && count > 0 {
801                 return fis, io.EOF
802         } else {
803                 return fis, nil
804         }
805 }
806
807 type fakeFS map[string]*fakeFileInfo
808
809 func (fsys fakeFS) Open(name string) (File, error) {
810         name = path.Clean(name)
811         f, ok := fsys[name]
812         if !ok {
813                 return nil, fs.ErrNotExist
814         }
815         if f.err != nil {
816                 return nil, f.err
817         }
818         return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil
819 }
820
821 func TestDirectoryIfNotModified(t *testing.T) { run(t, testDirectoryIfNotModified) }
822 func testDirectoryIfNotModified(t *testing.T, mode testMode) {
823         const indexContents = "I am a fake index.html file"
824         fileMod := time.Unix(1000000000, 0).UTC()
825         fileModStr := fileMod.Format(TimeFormat)
826         dirMod := time.Unix(123, 0).UTC()
827         indexFile := &fakeFileInfo{
828                 basename: "index.html",
829                 modtime:  fileMod,
830                 contents: indexContents,
831         }
832         fs := fakeFS{
833                 "/": &fakeFileInfo{
834                         dir:     true,
835                         modtime: dirMod,
836                         ents:    []*fakeFileInfo{indexFile},
837                 },
838                 "/index.html": indexFile,
839         }
840
841         ts := newClientServerTest(t, mode, FileServer(fs)).ts
842
843         res, err := ts.Client().Get(ts.URL)
844         if err != nil {
845                 t.Fatal(err)
846         }
847         b, err := io.ReadAll(res.Body)
848         if err != nil {
849                 t.Fatal(err)
850         }
851         if string(b) != indexContents {
852                 t.Fatalf("Got body %q; want %q", b, indexContents)
853         }
854         res.Body.Close()
855
856         lastMod := res.Header.Get("Last-Modified")
857         if lastMod != fileModStr {
858                 t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr)
859         }
860
861         req, _ := NewRequest("GET", ts.URL, nil)
862         req.Header.Set("If-Modified-Since", lastMod)
863
864         c := ts.Client()
865         res, err = c.Do(req)
866         if err != nil {
867                 t.Fatal(err)
868         }
869         if res.StatusCode != 304 {
870                 t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode)
871         }
872         res.Body.Close()
873
874         // Advance the index.html file's modtime, but not the directory's.
875         indexFile.modtime = indexFile.modtime.Add(1 * time.Hour)
876
877         res, err = c.Do(req)
878         if err != nil {
879                 t.Fatal(err)
880         }
881         if res.StatusCode != 200 {
882                 t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res)
883         }
884         res.Body.Close()
885 }
886
887 func mustStat(t *testing.T, fileName string) fs.FileInfo {
888         fi, err := os.Stat(fileName)
889         if err != nil {
890                 t.Fatal(err)
891         }
892         return fi
893 }
894
895 func TestServeContent(t *testing.T) { run(t, testServeContent) }
896 func testServeContent(t *testing.T, mode testMode) {
897         type serveParam struct {
898                 name        string
899                 modtime     time.Time
900                 content     io.ReadSeeker
901                 contentType string
902                 etag        string
903         }
904         servec := make(chan serveParam, 1)
905         ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
906                 p := <-servec
907                 if p.etag != "" {
908                         w.Header().Set("ETag", p.etag)
909                 }
910                 if p.contentType != "" {
911                         w.Header().Set("Content-Type", p.contentType)
912                 }
913                 ServeContent(w, r, p.name, p.modtime, p.content)
914         })).ts
915
916         type testCase struct {
917                 // One of file or content must be set:
918                 file    string
919                 content io.ReadSeeker
920
921                 modtime          time.Time
922                 serveETag        string // optional
923                 serveContentType string // optional
924                 reqHeader        map[string]string
925                 wantLastMod      string
926                 wantContentType  string
927                 wantContentRange string
928                 wantStatus       int
929         }
930         htmlModTime := mustStat(t, "testdata/index.html").ModTime()
931         tests := map[string]testCase{
932                 "no_last_modified": {
933                         file:            "testdata/style.css",
934                         wantContentType: "text/css; charset=utf-8",
935                         wantStatus:      200,
936                 },
937                 "with_last_modified": {
938                         file:            "testdata/index.html",
939                         wantContentType: "text/html; charset=utf-8",
940                         modtime:         htmlModTime,
941                         wantLastMod:     htmlModTime.UTC().Format(TimeFormat),
942                         wantStatus:      200,
943                 },
944                 "not_modified_modtime": {
945                         file:      "testdata/style.css",
946                         serveETag: `"foo"`, // Last-Modified sent only when no ETag
947                         modtime:   htmlModTime,
948                         reqHeader: map[string]string{
949                                 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
950                         },
951                         wantStatus: 304,
952                 },
953                 "not_modified_modtime_with_contenttype": {
954                         file:             "testdata/style.css",
955                         serveContentType: "text/css", // explicit content type
956                         serveETag:        `"foo"`,    // Last-Modified sent only when no ETag
957                         modtime:          htmlModTime,
958                         reqHeader: map[string]string{
959                                 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
960                         },
961                         wantStatus: 304,
962                 },
963                 "not_modified_etag": {
964                         file:      "testdata/style.css",
965                         serveETag: `"foo"`,
966                         reqHeader: map[string]string{
967                                 "If-None-Match": `"foo"`,
968                         },
969                         wantStatus: 304,
970                 },
971                 "not_modified_etag_no_seek": {
972                         content:   panicOnSeek{nil}, // should never be called
973                         serveETag: `W/"foo"`,        // If-None-Match uses weak ETag comparison
974                         reqHeader: map[string]string{
975                                 "If-None-Match": `"baz", W/"foo"`,
976                         },
977                         wantStatus: 304,
978                 },
979                 "if_none_match_mismatch": {
980                         file:      "testdata/style.css",
981                         serveETag: `"foo"`,
982                         reqHeader: map[string]string{
983                                 "If-None-Match": `"Foo"`,
984                         },
985                         wantStatus:      200,
986                         wantContentType: "text/css; charset=utf-8",
987                 },
988                 "if_none_match_malformed": {
989                         file:      "testdata/style.css",
990                         serveETag: `"foo"`,
991                         reqHeader: map[string]string{
992                                 "If-None-Match": `,`,
993                         },
994                         wantStatus:      200,
995                         wantContentType: "text/css; charset=utf-8",
996                 },
997                 "range_good": {
998                         file:      "testdata/style.css",
999                         serveETag: `"A"`,
1000                         reqHeader: map[string]string{
1001                                 "Range": "bytes=0-4",
1002                         },
1003                         wantStatus:       StatusPartialContent,
1004                         wantContentType:  "text/css; charset=utf-8",
1005                         wantContentRange: "bytes 0-4/8",
1006                 },
1007                 "range_match": {
1008                         file:      "testdata/style.css",
1009                         serveETag: `"A"`,
1010                         reqHeader: map[string]string{
1011                                 "Range":    "bytes=0-4",
1012                                 "If-Range": `"A"`,
1013                         },
1014                         wantStatus:       StatusPartialContent,
1015                         wantContentType:  "text/css; charset=utf-8",
1016                         wantContentRange: "bytes 0-4/8",
1017                 },
1018                 "range_match_weak_etag": {
1019                         file:      "testdata/style.css",
1020                         serveETag: `W/"A"`,
1021                         reqHeader: map[string]string{
1022                                 "Range":    "bytes=0-4",
1023                                 "If-Range": `W/"A"`,
1024                         },
1025                         wantStatus:      200,
1026                         wantContentType: "text/css; charset=utf-8",
1027                 },
1028                 "range_no_overlap": {
1029                         file:      "testdata/style.css",
1030                         serveETag: `"A"`,
1031                         reqHeader: map[string]string{
1032                                 "Range": "bytes=10-20",
1033                         },
1034                         wantStatus:       StatusRequestedRangeNotSatisfiable,
1035                         wantContentType:  "text/plain; charset=utf-8",
1036                         wantContentRange: "bytes */8",
1037                 },
1038                 // An If-Range resource for entity "A", but entity "B" is now current.
1039                 // The Range request should be ignored.
1040                 "range_no_match": {
1041                         file:      "testdata/style.css",
1042                         serveETag: `"A"`,
1043                         reqHeader: map[string]string{
1044                                 "Range":    "bytes=0-4",
1045                                 "If-Range": `"B"`,
1046                         },
1047                         wantStatus:      200,
1048                         wantContentType: "text/css; charset=utf-8",
1049                 },
1050                 "range_with_modtime": {
1051                         file:    "testdata/style.css",
1052                         modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC),
1053                         reqHeader: map[string]string{
1054                                 "Range":    "bytes=0-4",
1055                                 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
1056                         },
1057                         wantStatus:       StatusPartialContent,
1058                         wantContentType:  "text/css; charset=utf-8",
1059                         wantContentRange: "bytes 0-4/8",
1060                         wantLastMod:      "Wed, 25 Jun 2014 17:12:18 GMT",
1061                 },
1062                 "range_with_modtime_mismatch": {
1063                         file:    "testdata/style.css",
1064                         modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC),
1065                         reqHeader: map[string]string{
1066                                 "Range":    "bytes=0-4",
1067                                 "If-Range": "Wed, 25 Jun 2014 17:12:19 GMT",
1068                         },
1069                         wantStatus:      StatusOK,
1070                         wantContentType: "text/css; charset=utf-8",
1071                         wantLastMod:     "Wed, 25 Jun 2014 17:12:18 GMT",
1072                 },
1073                 "range_with_modtime_nanos": {
1074                         file:    "testdata/style.css",
1075                         modtime: time.Date(2014, 6, 25, 17, 12, 18, 123 /* nanos */, time.UTC),
1076                         reqHeader: map[string]string{
1077                                 "Range":    "bytes=0-4",
1078                                 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
1079                         },
1080                         wantStatus:       StatusPartialContent,
1081                         wantContentType:  "text/css; charset=utf-8",
1082                         wantContentRange: "bytes 0-4/8",
1083                         wantLastMod:      "Wed, 25 Jun 2014 17:12:18 GMT",
1084                 },
1085                 "unix_zero_modtime": {
1086                         content:         strings.NewReader("<html>foo"),
1087                         modtime:         time.Unix(0, 0),
1088                         wantStatus:      StatusOK,
1089                         wantContentType: "text/html; charset=utf-8",
1090                 },
1091                 "ifmatch_matches": {
1092                         file:      "testdata/style.css",
1093                         serveETag: `"A"`,
1094                         reqHeader: map[string]string{
1095                                 "If-Match": `"Z", "A"`,
1096                         },
1097                         wantStatus:      200,
1098                         wantContentType: "text/css; charset=utf-8",
1099                 },
1100                 "ifmatch_star": {
1101                         file:      "testdata/style.css",
1102                         serveETag: `"A"`,
1103                         reqHeader: map[string]string{
1104                                 "If-Match": `*`,
1105                         },
1106                         wantStatus:      200,
1107                         wantContentType: "text/css; charset=utf-8",
1108                 },
1109                 "ifmatch_failed": {
1110                         file:      "testdata/style.css",
1111                         serveETag: `"A"`,
1112                         reqHeader: map[string]string{
1113                                 "If-Match": `"B"`,
1114                         },
1115                         wantStatus: 412,
1116                 },
1117                 "ifmatch_fails_on_weak_etag": {
1118                         file:      "testdata/style.css",
1119                         serveETag: `W/"A"`,
1120                         reqHeader: map[string]string{
1121                                 "If-Match": `W/"A"`,
1122                         },
1123                         wantStatus: 412,
1124                 },
1125                 "if_unmodified_since_true": {
1126                         file:    "testdata/style.css",
1127                         modtime: htmlModTime,
1128                         reqHeader: map[string]string{
1129                                 "If-Unmodified-Since": htmlModTime.UTC().Format(TimeFormat),
1130                         },
1131                         wantStatus:      200,
1132                         wantContentType: "text/css; charset=utf-8",
1133                         wantLastMod:     htmlModTime.UTC().Format(TimeFormat),
1134                 },
1135                 "if_unmodified_since_false": {
1136                         file:    "testdata/style.css",
1137                         modtime: htmlModTime,
1138                         reqHeader: map[string]string{
1139                                 "If-Unmodified-Since": htmlModTime.Add(-2 * time.Second).UTC().Format(TimeFormat),
1140                         },
1141                         wantStatus:  412,
1142                         wantLastMod: htmlModTime.UTC().Format(TimeFormat),
1143                 },
1144         }
1145         for testName, tt := range tests {
1146                 var content io.ReadSeeker
1147                 if tt.file != "" {
1148                         f, err := os.Open(tt.file)
1149                         if err != nil {
1150                                 t.Fatalf("test %q: %v", testName, err)
1151                         }
1152                         defer f.Close()
1153                         content = f
1154                 } else {
1155                         content = tt.content
1156                 }
1157                 for _, method := range []string{"GET", "HEAD"} {
1158                         //restore content in case it is consumed by previous method
1159                         if content, ok := content.(*strings.Reader); ok {
1160                                 content.Seek(0, io.SeekStart)
1161                         }
1162
1163                         servec <- serveParam{
1164                                 name:        filepath.Base(tt.file),
1165                                 content:     content,
1166                                 modtime:     tt.modtime,
1167                                 etag:        tt.serveETag,
1168                                 contentType: tt.serveContentType,
1169                         }
1170                         req, err := NewRequest(method, ts.URL, nil)
1171                         if err != nil {
1172                                 t.Fatal(err)
1173                         }
1174                         for k, v := range tt.reqHeader {
1175                                 req.Header.Set(k, v)
1176                         }
1177
1178                         c := ts.Client()
1179                         res, err := c.Do(req)
1180                         if err != nil {
1181                                 t.Fatal(err)
1182                         }
1183                         io.Copy(io.Discard, res.Body)
1184                         res.Body.Close()
1185                         if res.StatusCode != tt.wantStatus {
1186                                 t.Errorf("test %q using %q: got status = %d; want %d", testName, method, res.StatusCode, tt.wantStatus)
1187                         }
1188                         if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e {
1189                                 t.Errorf("test %q using %q: got content-type = %q, want %q", testName, method, g, e)
1190                         }
1191                         if g, e := res.Header.Get("Content-Range"), tt.wantContentRange; g != e {
1192                                 t.Errorf("test %q using %q: got content-range = %q, want %q", testName, method, g, e)
1193                         }
1194                         if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e {
1195                                 t.Errorf("test %q using %q: got last-modified = %q, want %q", testName, method, g, e)
1196                         }
1197                 }
1198         }
1199 }
1200
1201 // Issue 12991
1202 func TestServerFileStatError(t *testing.T) {
1203         rec := httptest.NewRecorder()
1204         r, _ := NewRequest("GET", "http://foo/", nil)
1205         redirect := false
1206         name := "file.txt"
1207         fs := issue12991FS{}
1208         ExportServeFile(rec, r, fs, name, redirect)
1209         if body := rec.Body.String(); !strings.Contains(body, "403") || !strings.Contains(body, "Forbidden") {
1210                 t.Errorf("wanted 403 forbidden message; got: %s", body)
1211         }
1212 }
1213
1214 type issue12991FS struct{}
1215
1216 func (issue12991FS) Open(string) (File, error) { return issue12991File{}, nil }
1217
1218 type issue12991File struct{ File }
1219
1220 func (issue12991File) Stat() (fs.FileInfo, error) { return nil, fs.ErrPermission }
1221 func (issue12991File) Close() error               { return nil }
1222
1223 func TestServeContentErrorMessages(t *testing.T) { run(t, testServeContentErrorMessages) }
1224 func testServeContentErrorMessages(t *testing.T, mode testMode) {
1225         fs := fakeFS{
1226                 "/500": &fakeFileInfo{
1227                         err: errors.New("random error"),
1228                 },
1229                 "/403": &fakeFileInfo{
1230                         err: &fs.PathError{Err: fs.ErrPermission},
1231                 },
1232         }
1233         ts := newClientServerTest(t, mode, FileServer(fs)).ts
1234         c := ts.Client()
1235         for _, code := range []int{403, 404, 500} {
1236                 res, err := c.Get(fmt.Sprintf("%s/%d", ts.URL, code))
1237                 if err != nil {
1238                         t.Errorf("Error fetching /%d: %v", code, err)
1239                         continue
1240                 }
1241                 if res.StatusCode != code {
1242                         t.Errorf("For /%d, status code = %d; want %d", code, res.StatusCode, code)
1243                 }
1244                 res.Body.Close()
1245         }
1246 }
1247
1248 // verifies that sendfile is being used on Linux
1249 func TestLinuxSendfile(t *testing.T) {
1250         setParallel(t)
1251         defer afterTest(t)
1252         if runtime.GOOS != "linux" {
1253                 t.Skip("skipping; linux-only test")
1254         }
1255         if _, err := exec.LookPath("strace"); err != nil {
1256                 t.Skip("skipping; strace not found in path")
1257         }
1258
1259         ln, err := net.Listen("tcp", "127.0.0.1:0")
1260         if err != nil {
1261                 t.Fatal(err)
1262         }
1263         lnf, err := ln.(*net.TCPListener).File()
1264         if err != nil {
1265                 t.Fatal(err)
1266         }
1267         defer ln.Close()
1268
1269         // Attempt to run strace, and skip on failure - this test requires SYS_PTRACE.
1270         if err := testenv.Command(t, "strace", "-f", "-q", os.Args[0], "-test.run=^$").Run(); err != nil {
1271                 t.Skipf("skipping; failed to run strace: %v", err)
1272         }
1273
1274         filename := fmt.Sprintf("1kb-%d", os.Getpid())
1275         filepath := path.Join(os.TempDir(), filename)
1276
1277         if err := os.WriteFile(filepath, bytes.Repeat([]byte{'a'}, 1<<10), 0755); err != nil {
1278                 t.Fatal(err)
1279         }
1280         defer os.Remove(filepath)
1281
1282         var buf strings.Builder
1283         child := testenv.Command(t, "strace", "-f", "-q", os.Args[0], "-test.run=^TestLinuxSendfileChild$")
1284         child.ExtraFiles = append(child.ExtraFiles, lnf)
1285         child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
1286         child.Stdout = &buf
1287         child.Stderr = &buf
1288         if err := child.Start(); err != nil {
1289                 t.Skipf("skipping; failed to start straced child: %v", err)
1290         }
1291
1292         res, err := Get(fmt.Sprintf("http://%s/%s", ln.Addr(), filename))
1293         if err != nil {
1294                 t.Fatalf("http client error: %v", err)
1295         }
1296         _, err = io.Copy(io.Discard, res.Body)
1297         if err != nil {
1298                 t.Fatalf("client body read error: %v", err)
1299         }
1300         res.Body.Close()
1301
1302         // Force child to exit cleanly.
1303         Post(fmt.Sprintf("http://%s/quit", ln.Addr()), "", nil)
1304         child.Wait()
1305
1306         rx := regexp.MustCompile(`\b(n64:)?sendfile(64)?\(`)
1307         out := buf.String()
1308         if !rx.MatchString(out) {
1309                 t.Errorf("no sendfile system call found in:\n%s", out)
1310         }
1311 }
1312
1313 func getBody(t *testing.T, testName string, req Request, client *Client) (*Response, []byte) {
1314         r, err := client.Do(&req)
1315         if err != nil {
1316                 t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err)
1317         }
1318         b, err := io.ReadAll(r.Body)
1319         if err != nil {
1320                 t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err)
1321         }
1322         return r, b
1323 }
1324
1325 // TestLinuxSendfileChild isn't a real test. It's used as a helper process
1326 // for TestLinuxSendfile.
1327 func TestLinuxSendfileChild(*testing.T) {
1328         if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
1329                 return
1330         }
1331         defer os.Exit(0)
1332         fd3 := os.NewFile(3, "ephemeral-port-listener")
1333         ln, err := net.FileListener(fd3)
1334         if err != nil {
1335                 panic(err)
1336         }
1337         mux := NewServeMux()
1338         mux.Handle("/", FileServer(Dir(os.TempDir())))
1339         mux.HandleFunc("/quit", func(ResponseWriter, *Request) {
1340                 os.Exit(0)
1341         })
1342         s := &Server{Handler: mux}
1343         err = s.Serve(ln)
1344         if err != nil {
1345                 panic(err)
1346         }
1347 }
1348
1349 // Issues 18984, 49552: tests that requests for paths beyond files return not-found errors
1350 func TestFileServerNotDirError(t *testing.T) {
1351         run(t, func(t *testing.T, mode testMode) {
1352                 t.Run("Dir", func(t *testing.T) {
1353                         testFileServerNotDirError(t, mode, func(path string) FileSystem { return Dir(path) })
1354                 })
1355                 t.Run("FS", func(t *testing.T) {
1356                         testFileServerNotDirError(t, mode, func(path string) FileSystem { return FS(os.DirFS(path)) })
1357                 })
1358         })
1359 }
1360
1361 func testFileServerNotDirError(t *testing.T, mode testMode, newfs func(string) FileSystem) {
1362         ts := newClientServerTest(t, mode, FileServer(newfs("testdata"))).ts
1363
1364         res, err := ts.Client().Get(ts.URL + "/index.html/not-a-file")
1365         if err != nil {
1366                 t.Fatal(err)
1367         }
1368         res.Body.Close()
1369         if res.StatusCode != 404 {
1370                 t.Errorf("StatusCode = %v; want 404", res.StatusCode)
1371         }
1372
1373         test := func(name string, fsys FileSystem) {
1374                 t.Run(name, func(t *testing.T) {
1375                         _, err = fsys.Open("/index.html/not-a-file")
1376                         if err == nil {
1377                                 t.Fatal("err == nil; want != nil")
1378                         }
1379                         if !errors.Is(err, fs.ErrNotExist) {
1380                                 t.Errorf("err = %v; errors.Is(err, fs.ErrNotExist) = %v; want true", err,
1381                                         errors.Is(err, fs.ErrNotExist))
1382                         }
1383
1384                         _, err = fsys.Open("/index.html/not-a-dir/not-a-file")
1385                         if err == nil {
1386                                 t.Fatal("err == nil; want != nil")
1387                         }
1388                         if !errors.Is(err, fs.ErrNotExist) {
1389                                 t.Errorf("err = %v; errors.Is(err, fs.ErrNotExist) = %v; want true", err,
1390                                         errors.Is(err, fs.ErrNotExist))
1391                         }
1392                 })
1393         }
1394
1395         absPath, err := filepath.Abs("testdata")
1396         if err != nil {
1397                 t.Fatal("get abs path:", err)
1398         }
1399
1400         test("RelativePath", newfs("testdata"))
1401         test("AbsolutePath", newfs(absPath))
1402 }
1403
1404 func TestFileServerCleanPath(t *testing.T) {
1405         tests := []struct {
1406                 path     string
1407                 wantCode int
1408                 wantOpen []string
1409         }{
1410                 {"/", 200, []string{"/", "/index.html"}},
1411                 {"/dir", 301, []string{"/dir"}},
1412                 {"/dir/", 200, []string{"/dir", "/dir/index.html"}},
1413         }
1414         for _, tt := range tests {
1415                 var log []string
1416                 rr := httptest.NewRecorder()
1417                 req, _ := NewRequest("GET", "http://foo.localhost"+tt.path, nil)
1418                 FileServer(fileServerCleanPathDir{&log}).ServeHTTP(rr, req)
1419                 if !reflect.DeepEqual(log, tt.wantOpen) {
1420                         t.Logf("For %s: Opens = %q; want %q", tt.path, log, tt.wantOpen)
1421                 }
1422                 if rr.Code != tt.wantCode {
1423                         t.Logf("For %s: Response code = %d; want %d", tt.path, rr.Code, tt.wantCode)
1424                 }
1425         }
1426 }
1427
1428 type fileServerCleanPathDir struct {
1429         log *[]string
1430 }
1431
1432 func (d fileServerCleanPathDir) Open(path string) (File, error) {
1433         *(d.log) = append(*(d.log), path)
1434         if path == "/" || path == "/dir" || path == "/dir/" {
1435                 // Just return back something that's a directory.
1436                 return Dir(".").Open(".")
1437         }
1438         return nil, fs.ErrNotExist
1439 }
1440
1441 type panicOnSeek struct{ io.ReadSeeker }
1442
1443 func Test_scanETag(t *testing.T) {
1444         tests := []struct {
1445                 in         string
1446                 wantETag   string
1447                 wantRemain string
1448         }{
1449                 {`W/"etag-1"`, `W/"etag-1"`, ""},
1450                 {`"etag-2"`, `"etag-2"`, ""},
1451                 {`"etag-1", "etag-2"`, `"etag-1"`, `, "etag-2"`},
1452                 {"", "", ""},
1453                 {"W/", "", ""},
1454                 {`W/"truc`, "", ""},
1455                 {`w/"case-sensitive"`, "", ""},
1456                 {`"spaced etag"`, "", ""},
1457         }
1458         for _, test := range tests {
1459                 etag, remain := ExportScanETag(test.in)
1460                 if etag != test.wantETag || remain != test.wantRemain {
1461                         t.Errorf("scanETag(%q)=%q %q, want %q %q", test.in, etag, remain, test.wantETag, test.wantRemain)
1462                 }
1463         }
1464 }
1465
1466 // Issue 40940: Ensure that we only accept non-negative suffix-lengths
1467 // in "Range": "bytes=-N", and should reject "bytes=--2".
1468 func TestServeFileRejectsInvalidSuffixLengths(t *testing.T) {
1469         run(t, testServeFileRejectsInvalidSuffixLengths, []testMode{http1Mode, https1Mode, http2Mode})
1470 }
1471 func testServeFileRejectsInvalidSuffixLengths(t *testing.T, mode testMode) {
1472         cst := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts
1473
1474         tests := []struct {
1475                 r        string
1476                 wantCode int
1477                 wantBody string
1478         }{
1479                 {"bytes=--6", 416, "invalid range\n"},
1480                 {"bytes=--0", 416, "invalid range\n"},
1481                 {"bytes=---0", 416, "invalid range\n"},
1482                 {"bytes=-6", 206, "hello\n"},
1483                 {"bytes=6-", 206, "html says hello\n"},
1484                 {"bytes=-6-", 416, "invalid range\n"},
1485                 {"bytes=-0", 206, ""},
1486                 {"bytes=", 200, "index.html says hello\n"},
1487         }
1488
1489         for _, tt := range tests {
1490                 tt := tt
1491                 t.Run(tt.r, func(t *testing.T) {
1492                         req, err := NewRequest("GET", cst.URL+"/index.html", nil)
1493                         if err != nil {
1494                                 t.Fatal(err)
1495                         }
1496                         req.Header.Set("Range", tt.r)
1497                         res, err := cst.Client().Do(req)
1498                         if err != nil {
1499                                 t.Fatal(err)
1500                         }
1501                         if g, w := res.StatusCode, tt.wantCode; g != w {
1502                                 t.Errorf("StatusCode mismatch: got %d want %d", g, w)
1503                         }
1504                         slurp, err := io.ReadAll(res.Body)
1505                         res.Body.Close()
1506                         if err != nil {
1507                                 t.Fatal(err)
1508                         }
1509                         if g, w := string(slurp), tt.wantBody; g != w {
1510                                 t.Fatalf("Content mismatch:\nGot:  %q\nWant: %q", g, w)
1511                         }
1512                 })
1513         }
1514 }
1515
1516 func TestFileServerMethods(t *testing.T) {
1517         run(t, testFileServerMethods)
1518 }
1519 func testFileServerMethods(t *testing.T, mode testMode) {
1520         ts := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts
1521
1522         file, err := os.ReadFile(testFile)
1523         if err != nil {
1524                 t.Fatal("reading file:", err)
1525         }
1526
1527         // Get contents via various methods.
1528         //
1529         // See https://go.dev/issue/59471 for a proposal to limit the set of methods handled.
1530         // For now, test the historical behavior.
1531         for _, method := range []string{
1532                 MethodGet,
1533                 MethodHead,
1534                 MethodPost,
1535                 MethodPut,
1536                 MethodPatch,
1537                 MethodDelete,
1538                 MethodOptions,
1539                 MethodTrace,
1540         } {
1541                 req, _ := NewRequest(method, ts.URL+"/file", nil)
1542                 t.Log(req.URL)
1543                 res, err := ts.Client().Do(req)
1544                 if err != nil {
1545                         t.Fatal(err)
1546                 }
1547                 body, err := io.ReadAll(res.Body)
1548                 res.Body.Close()
1549                 if err != nil {
1550                         t.Fatal(err)
1551                 }
1552                 wantBody := file
1553                 if method == MethodHead {
1554                         wantBody = nil
1555                 }
1556                 if !bytes.Equal(body, wantBody) {
1557                         t.Fatalf("%v: got body %q, want %q", method, body, wantBody)
1558                 }
1559                 if got, want := res.Header.Get("Content-Length"), fmt.Sprint(len(file)); got != want {
1560                         t.Fatalf("%v: got Content-Length %q, want %q", method, got, want)
1561                 }
1562         }
1563 }
1564
1565 func TestFileServerFS(t *testing.T) {
1566         filename := "index.html"
1567         contents := []byte("index.html says hello")
1568         fsys := fstest.MapFS{
1569                 filename: {Data: contents},
1570         }
1571         ts := newClientServerTest(t, http1Mode, FileServerFS(fsys)).ts
1572         defer ts.Close()
1573
1574         res, err := ts.Client().Get(ts.URL + "/" + filename)
1575         if err != nil {
1576                 t.Fatal(err)
1577         }
1578         b, err := io.ReadAll(res.Body)
1579         if err != nil {
1580                 t.Fatal("reading Body:", err)
1581         }
1582         if s := string(b); s != string(contents) {
1583                 t.Errorf("for path %q got %q, want %q", filename, s, contents)
1584         }
1585         res.Body.Close()
1586 }
1587
1588 func TestServeFileFS(t *testing.T) {
1589         filename := "index.html"
1590         contents := []byte("index.html says hello")
1591         fsys := fstest.MapFS{
1592                 filename: {Data: contents},
1593         }
1594         ts := newClientServerTest(t, http1Mode, HandlerFunc(func(w ResponseWriter, r *Request) {
1595                 ServeFileFS(w, r, fsys, filename)
1596         })).ts
1597         defer ts.Close()
1598
1599         res, err := ts.Client().Get(ts.URL + "/" + filename)
1600         if err != nil {
1601                 t.Fatal(err)
1602         }
1603         b, err := io.ReadAll(res.Body)
1604         if err != nil {
1605                 t.Fatal("reading Body:", err)
1606         }
1607         if s := string(b); s != string(contents) {
1608                 t.Errorf("for path %q got %q, want %q", filename, s, contents)
1609         }
1610         res.Body.Close()
1611 }