github.com/tcnksm/go@v0.0.0-20141208075154-439b32936367/src/net/http/fs_test.go (about)

     1  // Copyright 2010 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package http_test
     6  
     7  import (
     8  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"mime"
    14  	"mime/multipart"
    15  	"net"
    16  	. "net/http"
    17  	"net/http/httptest"
    18  	"net/url"
    19  	"os"
    20  	"os/exec"
    21  	"path"
    22  	"path/filepath"
    23  	"reflect"
    24  	"regexp"
    25  	"runtime"
    26  	"strconv"
    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 itoa = strconv.Itoa
    42  
    43  var ServeFileRangeTests = []struct {
    44  	r      string
    45  	code   int
    46  	ranges []wantRange
    47  }{
    48  	{r: "", code: StatusOK},
    49  	{r: "bytes=0-4", code: StatusPartialContent, ranges: []wantRange{{0, 5}}},
    50  	{r: "bytes=2-", code: StatusPartialContent, ranges: []wantRange{{2, testFileLen}}},
    51  	{r: "bytes=-5", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 5, testFileLen}}},
    52  	{r: "bytes=3-7", code: StatusPartialContent, ranges: []wantRange{{3, 8}}},
    53  	{r: "bytes=20-", code: StatusRequestedRangeNotSatisfiable},
    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-" + itoa(testFileLen-2), code: StatusPartialContent, ranges: []wantRange{{0, testFileLen - 1}}},
    60  	{r: "bytes=0-" + itoa(testFileLen-1), code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
    61  	{r: "bytes=0-" + itoa(testFileLen), code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
    62  }
    63  
    64  func TestServeFile(t *testing.T) {
    65  	defer afterTest(t)
    66  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
    67  		ServeFile(w, r, "testdata/file")
    68  	}))
    69  	defer ts.Close()
    70  
    71  	var err error
    72  
    73  	file, err := ioutil.ReadFile(testFile)
    74  	if err != nil {
    75  		t.Fatal("reading file:", err)
    76  	}
    77  
    78  	// set up the Request (re-used for all tests)
    79  	var req Request
    80  	req.Header = make(Header)
    81  	if req.URL, err = url.Parse(ts.URL); err != nil {
    82  		t.Fatal("ParseURL:", err)
    83  	}
    84  	req.Method = "GET"
    85  
    86  	// straight GET
    87  	_, body := getBody(t, "straight get", req)
    88  	if !bytes.Equal(body, file) {
    89  		t.Fatalf("body mismatch: got %q, want %q", body, file)
    90  	}
    91  
    92  	// Range tests
    93  Cases:
    94  	for _, rt := range ServeFileRangeTests {
    95  		if rt.r != "" {
    96  			req.Header.Set("Range", rt.r)
    97  		}
    98  		resp, body := getBody(t, fmt.Sprintf("range test %q", rt.r), req)
    99  		if resp.StatusCode != rt.code {
   100  			t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, resp.StatusCode, rt.code)
   101  		}
   102  		if rt.code == StatusRequestedRangeNotSatisfiable {
   103  			continue
   104  		}
   105  		wantContentRange := ""
   106  		if len(rt.ranges) == 1 {
   107  			rng := rt.ranges[0]
   108  			wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
   109  		}
   110  		cr := resp.Header.Get("Content-Range")
   111  		if cr != wantContentRange {
   112  			t.Errorf("range=%q: Content-Range = %q, want %q", rt.r, cr, wantContentRange)
   113  		}
   114  		ct := resp.Header.Get("Content-Type")
   115  		if len(rt.ranges) == 1 {
   116  			rng := rt.ranges[0]
   117  			wantBody := file[rng.start:rng.end]
   118  			if !bytes.Equal(body, wantBody) {
   119  				t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
   120  			}
   121  			if strings.HasPrefix(ct, "multipart/byteranges") {
   122  				t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct)
   123  			}
   124  		}
   125  		if len(rt.ranges) > 1 {
   126  			typ, params, err := mime.ParseMediaType(ct)
   127  			if err != nil {
   128  				t.Errorf("range=%q content-type = %q; %v", rt.r, ct, err)
   129  				continue
   130  			}
   131  			if typ != "multipart/byteranges" {
   132  				t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ)
   133  				continue
   134  			}
   135  			if params["boundary"] == "" {
   136  				t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct)
   137  				continue
   138  			}
   139  			if g, w := resp.ContentLength, int64(len(body)); g != w {
   140  				t.Errorf("range=%q Content-Length = %d; want %d", rt.r, g, w)
   141  				continue
   142  			}
   143  			mr := multipart.NewReader(bytes.NewReader(body), params["boundary"])
   144  			for ri, rng := range rt.ranges {
   145  				part, err := mr.NextPart()
   146  				if err != nil {
   147  					t.Errorf("range=%q, reading part index %d: %v", rt.r, ri, err)
   148  					continue Cases
   149  				}
   150  				wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
   151  				if g, w := part.Header.Get("Content-Range"), wantContentRange; g != w {
   152  					t.Errorf("range=%q: part Content-Range = %q; want %q", rt.r, g, w)
   153  				}
   154  				body, err := ioutil.ReadAll(part)
   155  				if err != nil {
   156  					t.Errorf("range=%q, reading part index %d body: %v", rt.r, ri, err)
   157  					continue Cases
   158  				}
   159  				wantBody := file[rng.start:rng.end]
   160  				if !bytes.Equal(body, wantBody) {
   161  					t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
   162  				}
   163  			}
   164  			_, err = mr.NextPart()
   165  			if err != io.EOF {
   166  				t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err)
   167  			}
   168  		}
   169  	}
   170  }
   171  
   172  var fsRedirectTestData = []struct {
   173  	original, redirect string
   174  }{
   175  	{"/test/index.html", "/test/"},
   176  	{"/test/testdata", "/test/testdata/"},
   177  	{"/test/testdata/file/", "/test/testdata/file"},
   178  }
   179  
   180  func TestFSRedirect(t *testing.T) {
   181  	defer afterTest(t)
   182  	ts := httptest.NewServer(StripPrefix("/test", FileServer(Dir("."))))
   183  	defer ts.Close()
   184  
   185  	for _, data := range fsRedirectTestData {
   186  		res, err := Get(ts.URL + data.original)
   187  		if err != nil {
   188  			t.Fatal(err)
   189  		}
   190  		res.Body.Close()
   191  		if g, e := res.Request.URL.Path, data.redirect; g != e {
   192  			t.Errorf("redirect from %s: got %s, want %s", data.original, g, e)
   193  		}
   194  	}
   195  }
   196  
   197  type testFileSystem struct {
   198  	open func(name string) (File, error)
   199  }
   200  
   201  func (fs *testFileSystem) Open(name string) (File, error) {
   202  	return fs.open(name)
   203  }
   204  
   205  func TestFileServerCleans(t *testing.T) {
   206  	defer afterTest(t)
   207  	ch := make(chan string, 1)
   208  	fs := FileServer(&testFileSystem{func(name string) (File, error) {
   209  		ch <- name
   210  		return nil, errors.New("file does not exist")
   211  	}})
   212  	tests := []struct {
   213  		reqPath, openArg string
   214  	}{
   215  		{"/foo.txt", "/foo.txt"},
   216  		{"//foo.txt", "/foo.txt"},
   217  		{"/../foo.txt", "/foo.txt"},
   218  	}
   219  	req, _ := NewRequest("GET", "http://example.com", nil)
   220  	for n, test := range tests {
   221  		rec := httptest.NewRecorder()
   222  		req.URL.Path = test.reqPath
   223  		fs.ServeHTTP(rec, req)
   224  		if got := <-ch; got != test.openArg {
   225  			t.Errorf("test %d: got %q, want %q", n, got, test.openArg)
   226  		}
   227  	}
   228  }
   229  
   230  func TestFileServerEscapesNames(t *testing.T) {
   231  	defer afterTest(t)
   232  	const dirListPrefix = "<pre>\n"
   233  	const dirListSuffix = "\n</pre>\n"
   234  	tests := []struct {
   235  		name, escaped string
   236  	}{
   237  		{`simple_name`, `<a href="simple_name">simple_name</a>`},
   238  		{`"'<>&`, `<a href="%22%27%3C%3E&">&#34;&#39;&lt;&gt;&amp;</a>`},
   239  		{`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`},
   240  		{`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo">&lt;combo&gt;?foo</a>`},
   241  	}
   242  
   243  	// We put each test file in its own directory in the fakeFS so we can look at it in isolation.
   244  	fs := make(fakeFS)
   245  	for i, test := range tests {
   246  		testFile := &fakeFileInfo{basename: test.name}
   247  		fs[fmt.Sprintf("/%d", i)] = &fakeFileInfo{
   248  			dir:     true,
   249  			modtime: time.Unix(1000000000, 0).UTC(),
   250  			ents:    []*fakeFileInfo{testFile},
   251  		}
   252  		fs[fmt.Sprintf("/%d/%s", i, test.name)] = testFile
   253  	}
   254  
   255  	ts := httptest.NewServer(FileServer(&fs))
   256  	defer ts.Close()
   257  	for i, test := range tests {
   258  		url := fmt.Sprintf("%s/%d", ts.URL, i)
   259  		res, err := Get(url)
   260  		if err != nil {
   261  			t.Fatalf("test %q: Get: %v", test.name, err)
   262  		}
   263  		b, err := ioutil.ReadAll(res.Body)
   264  		if err != nil {
   265  			t.Fatalf("test %q: read Body: %v", test.name, err)
   266  		}
   267  		s := string(b)
   268  		if !strings.HasPrefix(s, dirListPrefix) || !strings.HasSuffix(s, dirListSuffix) {
   269  			t.Errorf("test %q: listing dir, full output is %q, want prefix %q and suffix %q", test.name, s, dirListPrefix, dirListSuffix)
   270  		}
   271  		if trimmed := strings.TrimSuffix(strings.TrimPrefix(s, dirListPrefix), dirListSuffix); trimmed != test.escaped {
   272  			t.Errorf("test %q: listing dir, filename escaped to %q, want %q", test.name, trimmed, test.escaped)
   273  		}
   274  		res.Body.Close()
   275  	}
   276  }
   277  
   278  func mustRemoveAll(dir string) {
   279  	err := os.RemoveAll(dir)
   280  	if err != nil {
   281  		panic(err)
   282  	}
   283  }
   284  
   285  func TestFileServerImplicitLeadingSlash(t *testing.T) {
   286  	defer afterTest(t)
   287  	tempDir, err := ioutil.TempDir("", "")
   288  	if err != nil {
   289  		t.Fatalf("TempDir: %v", err)
   290  	}
   291  	defer mustRemoveAll(tempDir)
   292  	if err := ioutil.WriteFile(filepath.Join(tempDir, "foo.txt"), []byte("Hello world"), 0644); err != nil {
   293  		t.Fatalf("WriteFile: %v", err)
   294  	}
   295  	ts := httptest.NewServer(StripPrefix("/bar/", FileServer(Dir(tempDir))))
   296  	defer ts.Close()
   297  	get := func(suffix string) string {
   298  		res, err := Get(ts.URL + suffix)
   299  		if err != nil {
   300  			t.Fatalf("Get %s: %v", suffix, err)
   301  		}
   302  		b, err := ioutil.ReadAll(res.Body)
   303  		if err != nil {
   304  			t.Fatalf("ReadAll %s: %v", suffix, err)
   305  		}
   306  		res.Body.Close()
   307  		return string(b)
   308  	}
   309  	if s := get("/bar/"); !strings.Contains(s, ">foo.txt<") {
   310  		t.Logf("expected a directory listing with foo.txt, got %q", s)
   311  	}
   312  	if s := get("/bar/foo.txt"); s != "Hello world" {
   313  		t.Logf("expected %q, got %q", "Hello world", s)
   314  	}
   315  }
   316  
   317  func TestDirJoin(t *testing.T) {
   318  	if runtime.GOOS == "windows" {
   319  		t.Skip("skipping test on windows")
   320  	}
   321  	wfi, err := os.Stat("/etc/hosts")
   322  	if err != nil {
   323  		t.Skip("skipping test; no /etc/hosts file")
   324  	}
   325  	test := func(d Dir, name string) {
   326  		f, err := d.Open(name)
   327  		if err != nil {
   328  			t.Fatalf("open of %s: %v", name, err)
   329  		}
   330  		defer f.Close()
   331  		gfi, err := f.Stat()
   332  		if err != nil {
   333  			t.Fatalf("stat of %s: %v", name, err)
   334  		}
   335  		if !os.SameFile(gfi, wfi) {
   336  			t.Errorf("%s got different file", name)
   337  		}
   338  	}
   339  	test(Dir("/etc/"), "/hosts")
   340  	test(Dir("/etc/"), "hosts")
   341  	test(Dir("/etc/"), "../../../../hosts")
   342  	test(Dir("/etc"), "/hosts")
   343  	test(Dir("/etc"), "hosts")
   344  	test(Dir("/etc"), "../../../../hosts")
   345  
   346  	// Not really directories, but since we use this trick in
   347  	// ServeFile, test it:
   348  	test(Dir("/etc/hosts"), "")
   349  	test(Dir("/etc/hosts"), "/")
   350  	test(Dir("/etc/hosts"), "../")
   351  }
   352  
   353  func TestEmptyDirOpenCWD(t *testing.T) {
   354  	test := func(d Dir) {
   355  		name := "fs_test.go"
   356  		f, err := d.Open(name)
   357  		if err != nil {
   358  			t.Fatalf("open of %s: %v", name, err)
   359  		}
   360  		defer f.Close()
   361  	}
   362  	test(Dir(""))
   363  	test(Dir("."))
   364  	test(Dir("./"))
   365  }
   366  
   367  func TestServeFileContentType(t *testing.T) {
   368  	defer afterTest(t)
   369  	const ctype = "icecream/chocolate"
   370  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   371  		switch r.FormValue("override") {
   372  		case "1":
   373  			w.Header().Set("Content-Type", ctype)
   374  		case "2":
   375  			// Explicitly inhibit sniffing.
   376  			w.Header()["Content-Type"] = []string{}
   377  		}
   378  		ServeFile(w, r, "testdata/file")
   379  	}))
   380  	defer ts.Close()
   381  	get := func(override string, want []string) {
   382  		resp, err := Get(ts.URL + "?override=" + override)
   383  		if err != nil {
   384  			t.Fatal(err)
   385  		}
   386  		if h := resp.Header["Content-Type"]; !reflect.DeepEqual(h, want) {
   387  			t.Errorf("Content-Type mismatch: got %v, want %v", h, want)
   388  		}
   389  		resp.Body.Close()
   390  	}
   391  	get("0", []string{"text/plain; charset=utf-8"})
   392  	get("1", []string{ctype})
   393  	get("2", nil)
   394  }
   395  
   396  func TestServeFileMimeType(t *testing.T) {
   397  	defer afterTest(t)
   398  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   399  		ServeFile(w, r, "testdata/style.css")
   400  	}))
   401  	defer ts.Close()
   402  	resp, err := Get(ts.URL)
   403  	if err != nil {
   404  		t.Fatal(err)
   405  	}
   406  	resp.Body.Close()
   407  	want := "text/css; charset=utf-8"
   408  	if h := resp.Header.Get("Content-Type"); h != want {
   409  		t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
   410  	}
   411  }
   412  
   413  func TestServeFileFromCWD(t *testing.T) {
   414  	defer afterTest(t)
   415  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   416  		ServeFile(w, r, "fs_test.go")
   417  	}))
   418  	defer ts.Close()
   419  	r, err := Get(ts.URL)
   420  	if err != nil {
   421  		t.Fatal(err)
   422  	}
   423  	r.Body.Close()
   424  	if r.StatusCode != 200 {
   425  		t.Fatalf("expected 200 OK, got %s", r.Status)
   426  	}
   427  }
   428  
   429  func TestServeFileWithContentEncoding(t *testing.T) {
   430  	defer afterTest(t)
   431  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   432  		w.Header().Set("Content-Encoding", "foo")
   433  		ServeFile(w, r, "testdata/file")
   434  	}))
   435  	defer ts.Close()
   436  	resp, err := Get(ts.URL)
   437  	if err != nil {
   438  		t.Fatal(err)
   439  	}
   440  	resp.Body.Close()
   441  	if g, e := resp.ContentLength, int64(-1); g != e {
   442  		t.Errorf("Content-Length mismatch: got %d, want %d", g, e)
   443  	}
   444  }
   445  
   446  func TestServeIndexHtml(t *testing.T) {
   447  	defer afterTest(t)
   448  	const want = "index.html says hello\n"
   449  	ts := httptest.NewServer(FileServer(Dir(".")))
   450  	defer ts.Close()
   451  
   452  	for _, path := range []string{"/testdata/", "/testdata/index.html"} {
   453  		res, err := Get(ts.URL + path)
   454  		if err != nil {
   455  			t.Fatal(err)
   456  		}
   457  		b, err := ioutil.ReadAll(res.Body)
   458  		if err != nil {
   459  			t.Fatal("reading Body:", err)
   460  		}
   461  		if s := string(b); s != want {
   462  			t.Errorf("for path %q got %q, want %q", path, s, want)
   463  		}
   464  		res.Body.Close()
   465  	}
   466  }
   467  
   468  func TestFileServerZeroByte(t *testing.T) {
   469  	defer afterTest(t)
   470  	ts := httptest.NewServer(FileServer(Dir(".")))
   471  	defer ts.Close()
   472  
   473  	res, err := Get(ts.URL + "/..\x00")
   474  	if err != nil {
   475  		t.Fatal(err)
   476  	}
   477  	b, err := ioutil.ReadAll(res.Body)
   478  	if err != nil {
   479  		t.Fatal("reading Body:", err)
   480  	}
   481  	if res.StatusCode == 200 {
   482  		t.Errorf("got status 200; want an error. Body is:\n%s", string(b))
   483  	}
   484  }
   485  
   486  type fakeFileInfo struct {
   487  	dir      bool
   488  	basename string
   489  	modtime  time.Time
   490  	ents     []*fakeFileInfo
   491  	contents string
   492  }
   493  
   494  func (f *fakeFileInfo) Name() string       { return f.basename }
   495  func (f *fakeFileInfo) Sys() interface{}   { return nil }
   496  func (f *fakeFileInfo) ModTime() time.Time { return f.modtime }
   497  func (f *fakeFileInfo) IsDir() bool        { return f.dir }
   498  func (f *fakeFileInfo) Size() int64        { return int64(len(f.contents)) }
   499  func (f *fakeFileInfo) Mode() os.FileMode {
   500  	if f.dir {
   501  		return 0755 | os.ModeDir
   502  	}
   503  	return 0644
   504  }
   505  
   506  type fakeFile struct {
   507  	io.ReadSeeker
   508  	fi     *fakeFileInfo
   509  	path   string // as opened
   510  	entpos int
   511  }
   512  
   513  func (f *fakeFile) Close() error               { return nil }
   514  func (f *fakeFile) Stat() (os.FileInfo, error) { return f.fi, nil }
   515  func (f *fakeFile) Readdir(count int) ([]os.FileInfo, error) {
   516  	if !f.fi.dir {
   517  		return nil, os.ErrInvalid
   518  	}
   519  	var fis []os.FileInfo
   520  
   521  	limit := f.entpos + count
   522  	if count <= 0 || limit > len(f.fi.ents) {
   523  		limit = len(f.fi.ents)
   524  	}
   525  	for ; f.entpos < limit; f.entpos++ {
   526  		fis = append(fis, f.fi.ents[f.entpos])
   527  	}
   528  
   529  	if len(fis) == 0 && count > 0 {
   530  		return fis, io.EOF
   531  	} else {
   532  		return fis, nil
   533  	}
   534  }
   535  
   536  type fakeFS map[string]*fakeFileInfo
   537  
   538  func (fs fakeFS) Open(name string) (File, error) {
   539  	name = path.Clean(name)
   540  	f, ok := fs[name]
   541  	if !ok {
   542  		return nil, os.ErrNotExist
   543  	}
   544  	return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil
   545  }
   546  
   547  func TestDirectoryIfNotModified(t *testing.T) {
   548  	defer afterTest(t)
   549  	const indexContents = "I am a fake index.html file"
   550  	fileMod := time.Unix(1000000000, 0).UTC()
   551  	fileModStr := fileMod.Format(TimeFormat)
   552  	dirMod := time.Unix(123, 0).UTC()
   553  	indexFile := &fakeFileInfo{
   554  		basename: "index.html",
   555  		modtime:  fileMod,
   556  		contents: indexContents,
   557  	}
   558  	fs := fakeFS{
   559  		"/": &fakeFileInfo{
   560  			dir:     true,
   561  			modtime: dirMod,
   562  			ents:    []*fakeFileInfo{indexFile},
   563  		},
   564  		"/index.html": indexFile,
   565  	}
   566  
   567  	ts := httptest.NewServer(FileServer(fs))
   568  	defer ts.Close()
   569  
   570  	res, err := Get(ts.URL)
   571  	if err != nil {
   572  		t.Fatal(err)
   573  	}
   574  	b, err := ioutil.ReadAll(res.Body)
   575  	if err != nil {
   576  		t.Fatal(err)
   577  	}
   578  	if string(b) != indexContents {
   579  		t.Fatalf("Got body %q; want %q", b, indexContents)
   580  	}
   581  	res.Body.Close()
   582  
   583  	lastMod := res.Header.Get("Last-Modified")
   584  	if lastMod != fileModStr {
   585  		t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr)
   586  	}
   587  
   588  	req, _ := NewRequest("GET", ts.URL, nil)
   589  	req.Header.Set("If-Modified-Since", lastMod)
   590  
   591  	res, err = DefaultClient.Do(req)
   592  	if err != nil {
   593  		t.Fatal(err)
   594  	}
   595  	if res.StatusCode != 304 {
   596  		t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode)
   597  	}
   598  	res.Body.Close()
   599  
   600  	// Advance the index.html file's modtime, but not the directory's.
   601  	indexFile.modtime = indexFile.modtime.Add(1 * time.Hour)
   602  
   603  	res, err = DefaultClient.Do(req)
   604  	if err != nil {
   605  		t.Fatal(err)
   606  	}
   607  	if res.StatusCode != 200 {
   608  		t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res)
   609  	}
   610  	res.Body.Close()
   611  }
   612  
   613  func mustStat(t *testing.T, fileName string) os.FileInfo {
   614  	fi, err := os.Stat(fileName)
   615  	if err != nil {
   616  		t.Fatal(err)
   617  	}
   618  	return fi
   619  }
   620  
   621  func TestServeContent(t *testing.T) {
   622  	defer afterTest(t)
   623  	type serveParam struct {
   624  		name        string
   625  		modtime     time.Time
   626  		content     io.ReadSeeker
   627  		contentType string
   628  		etag        string
   629  	}
   630  	servec := make(chan serveParam, 1)
   631  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   632  		p := <-servec
   633  		if p.etag != "" {
   634  			w.Header().Set("ETag", p.etag)
   635  		}
   636  		if p.contentType != "" {
   637  			w.Header().Set("Content-Type", p.contentType)
   638  		}
   639  		ServeContent(w, r, p.name, p.modtime, p.content)
   640  	}))
   641  	defer ts.Close()
   642  
   643  	type testCase struct {
   644  		// One of file or content must be set:
   645  		file    string
   646  		content io.ReadSeeker
   647  
   648  		modtime          time.Time
   649  		serveETag        string // optional
   650  		serveContentType string // optional
   651  		reqHeader        map[string]string
   652  		wantLastMod      string
   653  		wantContentType  string
   654  		wantStatus       int
   655  	}
   656  	htmlModTime := mustStat(t, "testdata/index.html").ModTime()
   657  	tests := map[string]testCase{
   658  		"no_last_modified": {
   659  			file:            "testdata/style.css",
   660  			wantContentType: "text/css; charset=utf-8",
   661  			wantStatus:      200,
   662  		},
   663  		"with_last_modified": {
   664  			file:            "testdata/index.html",
   665  			wantContentType: "text/html; charset=utf-8",
   666  			modtime:         htmlModTime,
   667  			wantLastMod:     htmlModTime.UTC().Format(TimeFormat),
   668  			wantStatus:      200,
   669  		},
   670  		"not_modified_modtime": {
   671  			file:    "testdata/style.css",
   672  			modtime: htmlModTime,
   673  			reqHeader: map[string]string{
   674  				"If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
   675  			},
   676  			wantStatus: 304,
   677  		},
   678  		"not_modified_modtime_with_contenttype": {
   679  			file:             "testdata/style.css",
   680  			serveContentType: "text/css", // explicit content type
   681  			modtime:          htmlModTime,
   682  			reqHeader: map[string]string{
   683  				"If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
   684  			},
   685  			wantStatus: 304,
   686  		},
   687  		"not_modified_etag": {
   688  			file:      "testdata/style.css",
   689  			serveETag: `"foo"`,
   690  			reqHeader: map[string]string{
   691  				"If-None-Match": `"foo"`,
   692  			},
   693  			wantStatus: 304,
   694  		},
   695  		"not_modified_etag_no_seek": {
   696  			content:   panicOnSeek{nil}, // should never be called
   697  			serveETag: `"foo"`,
   698  			reqHeader: map[string]string{
   699  				"If-None-Match": `"foo"`,
   700  			},
   701  			wantStatus: 304,
   702  		},
   703  		"range_good": {
   704  			file:      "testdata/style.css",
   705  			serveETag: `"A"`,
   706  			reqHeader: map[string]string{
   707  				"Range": "bytes=0-4",
   708  			},
   709  			wantStatus:      StatusPartialContent,
   710  			wantContentType: "text/css; charset=utf-8",
   711  		},
   712  		// An If-Range resource for entity "A", but entity "B" is now current.
   713  		// The Range request should be ignored.
   714  		"range_no_match": {
   715  			file:      "testdata/style.css",
   716  			serveETag: `"A"`,
   717  			reqHeader: map[string]string{
   718  				"Range":    "bytes=0-4",
   719  				"If-Range": `"B"`,
   720  			},
   721  			wantStatus:      200,
   722  			wantContentType: "text/css; charset=utf-8",
   723  		},
   724  		"range_with_modtime": {
   725  			file:    "testdata/style.css",
   726  			modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC),
   727  			reqHeader: map[string]string{
   728  				"Range":    "bytes=0-4",
   729  				"If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
   730  			},
   731  			wantStatus:      StatusPartialContent,
   732  			wantContentType: "text/css; charset=utf-8",
   733  			wantLastMod:     "Wed, 25 Jun 2014 17:12:18 GMT",
   734  		},
   735  		"range_with_modtime_nanos": {
   736  			file:    "testdata/style.css",
   737  			modtime: time.Date(2014, 6, 25, 17, 12, 18, 123 /* nanos */, time.UTC),
   738  			reqHeader: map[string]string{
   739  				"Range":    "bytes=0-4",
   740  				"If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
   741  			},
   742  			wantStatus:      StatusPartialContent,
   743  			wantContentType: "text/css; charset=utf-8",
   744  			wantLastMod:     "Wed, 25 Jun 2014 17:12:18 GMT",
   745  		},
   746  	}
   747  	for testName, tt := range tests {
   748  		var content io.ReadSeeker
   749  		if tt.file != "" {
   750  			f, err := os.Open(tt.file)
   751  			if err != nil {
   752  				t.Fatalf("test %q: %v", testName, err)
   753  			}
   754  			defer f.Close()
   755  			content = f
   756  		} else {
   757  			content = tt.content
   758  		}
   759  
   760  		servec <- serveParam{
   761  			name:        filepath.Base(tt.file),
   762  			content:     content,
   763  			modtime:     tt.modtime,
   764  			etag:        tt.serveETag,
   765  			contentType: tt.serveContentType,
   766  		}
   767  		req, err := NewRequest("GET", ts.URL, nil)
   768  		if err != nil {
   769  			t.Fatal(err)
   770  		}
   771  		for k, v := range tt.reqHeader {
   772  			req.Header.Set(k, v)
   773  		}
   774  		res, err := DefaultClient.Do(req)
   775  		if err != nil {
   776  			t.Fatal(err)
   777  		}
   778  		io.Copy(ioutil.Discard, res.Body)
   779  		res.Body.Close()
   780  		if res.StatusCode != tt.wantStatus {
   781  			t.Errorf("test %q: status = %d; want %d", testName, res.StatusCode, tt.wantStatus)
   782  		}
   783  		if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e {
   784  			t.Errorf("test %q: content-type = %q, want %q", testName, g, e)
   785  		}
   786  		if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e {
   787  			t.Errorf("test %q: last-modified = %q, want %q", testName, g, e)
   788  		}
   789  	}
   790  }
   791  
   792  // verifies that sendfile is being used on Linux
   793  func TestLinuxSendfile(t *testing.T) {
   794  	defer afterTest(t)
   795  	if runtime.GOOS != "linux" {
   796  		t.Skip("skipping; linux-only test")
   797  	}
   798  	if _, err := exec.LookPath("strace"); err != nil {
   799  		t.Skip("skipping; strace not found in path")
   800  	}
   801  
   802  	ln, err := net.Listen("tcp", "127.0.0.1:0")
   803  	if err != nil {
   804  		t.Fatal(err)
   805  	}
   806  	lnf, err := ln.(*net.TCPListener).File()
   807  	if err != nil {
   808  		t.Fatal(err)
   809  	}
   810  	defer ln.Close()
   811  
   812  	var buf bytes.Buffer
   813  	child := exec.Command("strace", "-f", "-q", "-e", "trace=sendfile,sendfile64", os.Args[0], "-test.run=TestLinuxSendfileChild")
   814  	child.ExtraFiles = append(child.ExtraFiles, lnf)
   815  	child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
   816  	child.Stdout = &buf
   817  	child.Stderr = &buf
   818  	if err := child.Start(); err != nil {
   819  		t.Skipf("skipping; failed to start straced child: %v", err)
   820  	}
   821  
   822  	res, err := Get(fmt.Sprintf("http://%s/", ln.Addr()))
   823  	if err != nil {
   824  		t.Fatalf("http client error: %v", err)
   825  	}
   826  	_, err = io.Copy(ioutil.Discard, res.Body)
   827  	if err != nil {
   828  		t.Fatalf("client body read error: %v", err)
   829  	}
   830  	res.Body.Close()
   831  
   832  	// Force child to exit cleanly.
   833  	Get(fmt.Sprintf("http://%s/quit", ln.Addr()))
   834  	child.Wait()
   835  
   836  	rx := regexp.MustCompile(`sendfile(64)?\(\d+,\s*\d+,\s*NULL,\s*\d+\)\s*=\s*\d+\s*\n`)
   837  	rxResume := regexp.MustCompile(`<\.\.\. sendfile(64)? resumed> \)\s*=\s*\d+\s*\n`)
   838  	out := buf.String()
   839  	if !rx.MatchString(out) && !rxResume.MatchString(out) {
   840  		t.Errorf("no sendfile system call found in:\n%s", out)
   841  	}
   842  }
   843  
   844  func getBody(t *testing.T, testName string, req Request) (*Response, []byte) {
   845  	r, err := DefaultClient.Do(&req)
   846  	if err != nil {
   847  		t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err)
   848  	}
   849  	b, err := ioutil.ReadAll(r.Body)
   850  	if err != nil {
   851  		t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err)
   852  	}
   853  	return r, b
   854  }
   855  
   856  // TestLinuxSendfileChild isn't a real test. It's used as a helper process
   857  // for TestLinuxSendfile.
   858  func TestLinuxSendfileChild(*testing.T) {
   859  	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
   860  		return
   861  	}
   862  	defer os.Exit(0)
   863  	fd3 := os.NewFile(3, "ephemeral-port-listener")
   864  	ln, err := net.FileListener(fd3)
   865  	if err != nil {
   866  		panic(err)
   867  	}
   868  	mux := NewServeMux()
   869  	mux.Handle("/", FileServer(Dir("testdata")))
   870  	mux.HandleFunc("/quit", func(ResponseWriter, *Request) {
   871  		os.Exit(0)
   872  	})
   873  	s := &Server{Handler: mux}
   874  	err = s.Serve(ln)
   875  	if err != nil {
   876  		panic(err)
   877  	}
   878  }
   879  
   880  func TestFileServerCleanPath(t *testing.T) {
   881  	tests := []struct {
   882  		path     string
   883  		wantCode int
   884  		wantOpen []string
   885  	}{
   886  		{"/", 200, []string{"/", "/index.html"}},
   887  		{"/dir", 301, []string{"/dir"}},
   888  		{"/dir/", 200, []string{"/dir", "/dir/index.html"}},
   889  	}
   890  	for _, tt := range tests {
   891  		var log []string
   892  		rr := httptest.NewRecorder()
   893  		req, _ := NewRequest("GET", "http://foo.localhost"+tt.path, nil)
   894  		FileServer(fileServerCleanPathDir{&log}).ServeHTTP(rr, req)
   895  		if !reflect.DeepEqual(log, tt.wantOpen) {
   896  			t.Logf("For %s: Opens = %q; want %q", tt.path, log, tt.wantOpen)
   897  		}
   898  		if rr.Code != tt.wantCode {
   899  			t.Logf("For %s: Response code = %d; want %d", tt.path, rr.Code, tt.wantCode)
   900  		}
   901  	}
   902  }
   903  
   904  type fileServerCleanPathDir struct {
   905  	log *[]string
   906  }
   907  
   908  func (d fileServerCleanPathDir) Open(path string) (File, error) {
   909  	*(d.log) = append(*(d.log), path)
   910  	if path == "/" || path == "/dir" || path == "/dir/" {
   911  		// Just return back something that's a directory.
   912  		return Dir(".").Open(".")
   913  	}
   914  	return nil, os.ErrNotExist
   915  }
   916  
   917  type panicOnSeek struct{ io.ReadSeeker }