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

View as plain text