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