...

Source file src/net/http/fs_test.go

Documentation: net/http

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

View as plain text