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