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