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