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