github.com/twelsh-aw/go/src@v0.0.0-20230516233729-a56fe86a7c81/net/http/fs_test.go (about)

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