2010-12-10 08:51:13 +11:00
|
|
|
// Copyright 2010 The Go Authors. All rights reserved.
|
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
2011-03-05 13:51:35 -08:00
|
|
|
package http_test
|
2010-12-10 08:51:13 +11:00
|
|
|
|
|
|
|
|
import (
|
2016-01-26 19:57:19 +00:00
|
|
|
"bufio"
|
2012-02-14 09:34:52 +11:00
|
|
|
"bytes"
|
2012-02-17 10:04:29 +11:00
|
|
|
"errors"
|
2010-12-10 08:51:13 +11:00
|
|
|
"fmt"
|
2012-02-10 10:02:06 +11:00
|
|
|
"io"
|
2010-12-10 08:51:13 +11:00
|
|
|
"io/ioutil"
|
2012-06-29 07:44:04 -07:00
|
|
|
"mime"
|
|
|
|
|
"mime/multipart"
|
2012-02-14 09:34:52 +11:00
|
|
|
"net"
|
2011-11-08 15:41:54 -08:00
|
|
|
. "net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"net/url"
|
2010-12-10 08:51:13 +11:00
|
|
|
"os"
|
2012-02-14 09:34:52 +11:00
|
|
|
"os/exec"
|
2012-06-13 14:53:05 -07:00
|
|
|
"path"
|
2011-07-18 09:04:48 -07:00
|
|
|
"path/filepath"
|
2013-10-15 08:22:04 +11:00
|
|
|
"reflect"
|
2012-02-14 09:34:52 +11:00
|
|
|
"regexp"
|
|
|
|
|
"runtime"
|
2011-07-18 09:04:48 -07:00
|
|
|
"strings"
|
2010-12-10 08:51:13 +11:00
|
|
|
"testing"
|
2012-02-10 10:02:06 +11:00
|
|
|
"time"
|
2010-12-10 08:51:13 +11:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
2012-06-29 07:44:04 -07:00
|
|
|
testFile = "testdata/file"
|
|
|
|
|
testFileLen = 11
|
2010-12-10 08:51:13 +11:00
|
|
|
)
|
|
|
|
|
|
2012-06-29 07:44:04 -07:00
|
|
|
type wantRange struct {
|
|
|
|
|
start, end int64 // range [start,end)
|
|
|
|
|
}
|
|
|
|
|
|
2010-12-10 08:51:13 +11:00
|
|
|
var ServeFileRangeTests = []struct {
|
2012-06-29 07:44:04 -07:00
|
|
|
r string
|
|
|
|
|
code int
|
|
|
|
|
ranges []wantRange
|
2010-12-10 08:51:13 +11:00
|
|
|
}{
|
2012-06-29 07:44:04 -07:00
|
|
|
{r: "", code: StatusOK},
|
|
|
|
|
{r: "bytes=0-4", code: StatusPartialContent, ranges: []wantRange{{0, 5}}},
|
|
|
|
|
{r: "bytes=2-", code: StatusPartialContent, ranges: []wantRange{{2, testFileLen}}},
|
|
|
|
|
{r: "bytes=-5", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 5, testFileLen}}},
|
|
|
|
|
{r: "bytes=3-7", code: StatusPartialContent, ranges: []wantRange{{3, 8}}},
|
|
|
|
|
{r: "bytes=0-0,-2", code: StatusPartialContent, ranges: []wantRange{{0, 1}, {testFileLen - 2, testFileLen}}},
|
|
|
|
|
{r: "bytes=0-1,5-8", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, 9}}},
|
|
|
|
|
{r: "bytes=0-1,5-", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, testFileLen}}},
|
2013-09-23 17:16:59 -04:00
|
|
|
{r: "bytes=5-1000", code: StatusPartialContent, ranges: []wantRange{{5, testFileLen}}},
|
2012-06-30 12:26:06 -07:00
|
|
|
{r: "bytes=0-,1-,2-,3-,4-", code: StatusOK}, // ignore wasteful range request
|
net/http: Fix Range off-by-one error
Given a file of size N, a request for "Range: bytes=N-*" should
return a 416 [1]. Currently, it returns a 206 and a body of 0
bytes, with the illegal Content-Range of "bytes N-(N-1)/N" [2].
[1]: RFC 7233, sec 2.1: "If a valid byte-range-set includes at least one
byte-range-spec with a first-byte-pos that is less than the current
length of the representation, [...]". sec 3.1: "If all of the
preconditions are true, the server supports the Range header field for
the target resource, and the specified range(s) are invalid or
unsatisfiable, the server SHOULD send a 416 (Range Not Satisfiable)
response."
[2]: RFC 7233, sec 4.2: "A Content-Range field value is invalid if it
contains a byte-range-resp that has a last-byte-pos value less than its
first-byte-pos value, [...]"
Fixes #8988
Change-Id: If3e1134e7815f5d361efea01873b29aafe3de817
Reviewed-on: https://go-review.googlesource.com/1862
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
2014-12-16 00:24:19 -08:00
|
|
|
{r: "bytes=0-9", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen - 1}}},
|
|
|
|
|
{r: "bytes=0-10", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
|
|
|
|
|
{r: "bytes=0-11", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
|
|
|
|
|
{r: "bytes=10-11", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
|
|
|
|
|
{r: "bytes=10-", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
|
|
|
|
|
{r: "bytes=11-", code: StatusRequestedRangeNotSatisfiable},
|
|
|
|
|
{r: "bytes=11-12", code: StatusRequestedRangeNotSatisfiable},
|
|
|
|
|
{r: "bytes=12-12", code: StatusRequestedRangeNotSatisfiable},
|
|
|
|
|
{r: "bytes=11-100", code: StatusRequestedRangeNotSatisfiable},
|
|
|
|
|
{r: "bytes=12-100", code: StatusRequestedRangeNotSatisfiable},
|
|
|
|
|
{r: "bytes=100-", code: StatusRequestedRangeNotSatisfiable},
|
|
|
|
|
{r: "bytes=100-1000", code: StatusRequestedRangeNotSatisfiable},
|
2010-12-10 08:51:13 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestServeFile(t *testing.T) {
|
2016-11-04 03:23:37 +00:00
|
|
|
setParallel(t)
|
2013-03-15 15:09:17 -07:00
|
|
|
defer afterTest(t)
|
2011-03-05 13:51:35 -08:00
|
|
|
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
|
|
|
|
|
ServeFile(w, r, "testdata/file")
|
|
|
|
|
}))
|
|
|
|
|
defer ts.Close()
|
2017-03-04 18:24:44 +00:00
|
|
|
c := ts.Client()
|
2011-03-05 13:51:35 -08:00
|
|
|
|
2011-11-01 22:04:37 -04:00
|
|
|
var err error
|
2010-12-10 08:51:13 +11:00
|
|
|
|
|
|
|
|
file, err := ioutil.ReadFile(testFile)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal("reading file:", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// set up the Request (re-used for all tests)
|
|
|
|
|
var req Request
|
2011-02-23 00:39:25 -05:00
|
|
|
req.Header = make(Header)
|
2011-08-17 13:36:02 +10:00
|
|
|
if req.URL, err = url.Parse(ts.URL); err != nil {
|
2010-12-10 08:51:13 +11:00
|
|
|
t.Fatal("ParseURL:", err)
|
|
|
|
|
}
|
|
|
|
|
req.Method = "GET"
|
|
|
|
|
|
|
|
|
|
// straight GET
|
2017-03-04 18:24:44 +00:00
|
|
|
_, body := getBody(t, "straight get", req, c)
|
2012-06-29 07:44:04 -07:00
|
|
|
if !bytes.Equal(body, file) {
|
2010-12-10 08:51:13 +11:00
|
|
|
t.Fatalf("body mismatch: got %q, want %q", body, file)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Range tests
|
2012-07-03 10:17:55 -07:00
|
|
|
Cases:
|
2012-06-29 07:44:04 -07:00
|
|
|
for _, rt := range ServeFileRangeTests {
|
|
|
|
|
if rt.r != "" {
|
|
|
|
|
req.Header.Set("Range", rt.r)
|
2010-12-10 08:51:13 +11:00
|
|
|
}
|
2017-03-04 18:24:44 +00:00
|
|
|
resp, body := getBody(t, fmt.Sprintf("range test %q", rt.r), req, c)
|
2012-06-29 07:44:04 -07:00
|
|
|
if resp.StatusCode != rt.code {
|
|
|
|
|
t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, resp.StatusCode, rt.code)
|
2010-12-10 08:51:13 +11:00
|
|
|
}
|
|
|
|
|
if rt.code == StatusRequestedRangeNotSatisfiable {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2012-06-29 07:44:04 -07:00
|
|
|
wantContentRange := ""
|
|
|
|
|
if len(rt.ranges) == 1 {
|
|
|
|
|
rng := rt.ranges[0]
|
|
|
|
|
wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
|
2010-12-10 08:51:13 +11:00
|
|
|
}
|
2012-06-29 07:44:04 -07:00
|
|
|
cr := resp.Header.Get("Content-Range")
|
|
|
|
|
if cr != wantContentRange {
|
|
|
|
|
t.Errorf("range=%q: Content-Range = %q, want %q", rt.r, cr, wantContentRange)
|
2010-12-10 08:51:13 +11:00
|
|
|
}
|
2012-06-29 07:44:04 -07:00
|
|
|
ct := resp.Header.Get("Content-Type")
|
|
|
|
|
if len(rt.ranges) == 1 {
|
|
|
|
|
rng := rt.ranges[0]
|
|
|
|
|
wantBody := file[rng.start:rng.end]
|
|
|
|
|
if !bytes.Equal(body, wantBody) {
|
|
|
|
|
t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(ct, "multipart/byteranges") {
|
2012-07-03 10:17:55 -07:00
|
|
|
t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct)
|
2012-06-29 07:44:04 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(rt.ranges) > 1 {
|
|
|
|
|
typ, params, err := mime.ParseMediaType(ct)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("range=%q content-type = %q; %v", rt.r, ct, err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if typ != "multipart/byteranges" {
|
2012-07-03 10:17:55 -07:00
|
|
|
t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ)
|
2012-06-29 07:44:04 -07:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if params["boundary"] == "" {
|
|
|
|
|
t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct)
|
2012-07-03 10:17:55 -07:00
|
|
|
continue
|
2012-06-29 07:44:04 -07:00
|
|
|
}
|
|
|
|
|
if g, w := resp.ContentLength, int64(len(body)); g != w {
|
|
|
|
|
t.Errorf("range=%q Content-Length = %d; want %d", rt.r, g, w)
|
2012-07-03 10:17:55 -07:00
|
|
|
continue
|
2012-06-29 07:44:04 -07:00
|
|
|
}
|
|
|
|
|
mr := multipart.NewReader(bytes.NewReader(body), params["boundary"])
|
|
|
|
|
for ri, rng := range rt.ranges {
|
|
|
|
|
part, err := mr.NextPart()
|
|
|
|
|
if err != nil {
|
2012-07-03 10:17:55 -07:00
|
|
|
t.Errorf("range=%q, reading part index %d: %v", rt.r, ri, err)
|
|
|
|
|
continue Cases
|
|
|
|
|
}
|
|
|
|
|
wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
|
|
|
|
|
if g, w := part.Header.Get("Content-Range"), wantContentRange; g != w {
|
|
|
|
|
t.Errorf("range=%q: part Content-Range = %q; want %q", rt.r, g, w)
|
2012-06-29 07:44:04 -07:00
|
|
|
}
|
|
|
|
|
body, err := ioutil.ReadAll(part)
|
|
|
|
|
if err != nil {
|
2012-07-03 10:17:55 -07:00
|
|
|
t.Errorf("range=%q, reading part index %d body: %v", rt.r, ri, err)
|
|
|
|
|
continue Cases
|
2012-06-29 07:44:04 -07:00
|
|
|
}
|
|
|
|
|
wantBody := file[rng.start:rng.end]
|
|
|
|
|
if !bytes.Equal(body, wantBody) {
|
|
|
|
|
t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_, err = mr.NextPart()
|
|
|
|
|
if err != io.EOF {
|
2012-07-03 10:17:55 -07:00
|
|
|
t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err)
|
2012-06-29 07:44:04 -07:00
|
|
|
}
|
2010-12-10 08:51:13 +11:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-01-26 19:57:19 +00:00
|
|
|
func TestServeFile_DotDot(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
req string
|
|
|
|
|
wantStatus int
|
|
|
|
|
}{
|
|
|
|
|
{"/testdata/file", 200},
|
|
|
|
|
{"/../file", 400},
|
|
|
|
|
{"/..", 400},
|
|
|
|
|
{"/../", 400},
|
|
|
|
|
{"/../foo", 400},
|
|
|
|
|
{"/..\\foo", 400},
|
|
|
|
|
{"/file/a", 200},
|
|
|
|
|
{"/file/a..", 200},
|
|
|
|
|
{"/file/a/..", 400},
|
|
|
|
|
{"/file/a\\..", 400},
|
|
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
req, err := ReadRequest(bufio.NewReader(strings.NewReader("GET " + tt.req + " HTTP/1.1\r\nHost: foo\r\n\r\n")))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("bad request %q: %v", tt.req, err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
|
ServeFile(rec, req, "testdata/file")
|
|
|
|
|
if rec.Code != tt.wantStatus {
|
|
|
|
|
t.Errorf("for request %q, status = %d; want %d", tt.req, rec.Code, tt.wantStatus)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2011-07-28 11:43:16 -07:00
|
|
|
var fsRedirectTestData = []struct {
|
|
|
|
|
original, redirect string
|
|
|
|
|
}{
|
|
|
|
|
{"/test/index.html", "/test/"},
|
|
|
|
|
{"/test/testdata", "/test/testdata/"},
|
|
|
|
|
{"/test/testdata/file/", "/test/testdata/file"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestFSRedirect(t *testing.T) {
|
2013-03-15 15:09:17 -07:00
|
|
|
defer afterTest(t)
|
2011-07-28 11:43:16 -07:00
|
|
|
ts := httptest.NewServer(StripPrefix("/test", FileServer(Dir("."))))
|
|
|
|
|
defer ts.Close()
|
|
|
|
|
|
|
|
|
|
for _, data := range fsRedirectTestData {
|
|
|
|
|
res, err := Get(ts.URL + data.original)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
if g, e := res.Request.URL.Path, data.redirect; g != e {
|
|
|
|
|
t.Errorf("redirect from %s: got %s, want %s", data.original, g, e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
http: add FileSystem interface, make FileServer use it
Permits serving from virtual filesystems, such as files linked
into a binary, or from a zip file.
Also adds a gofix for:
http.FileServer(root, prefix) -> http.StripPrefix(prefix, http.FileServer(http.Dir(root)))
R=r, rsc, gri, adg, dsymonds, r, gri
CC=golang-dev
https://golang.org/cl/4629047
2011-06-27 15:26:36 -07:00
|
|
|
type testFileSystem struct {
|
2011-11-01 22:04:37 -04:00
|
|
|
open func(name string) (File, error)
|
http: add FileSystem interface, make FileServer use it
Permits serving from virtual filesystems, such as files linked
into a binary, or from a zip file.
Also adds a gofix for:
http.FileServer(root, prefix) -> http.StripPrefix(prefix, http.FileServer(http.Dir(root)))
R=r, rsc, gri, adg, dsymonds, r, gri
CC=golang-dev
https://golang.org/cl/4629047
2011-06-27 15:26:36 -07:00
|
|
|
}
|
|
|
|
|
|
2011-11-01 22:04:37 -04:00
|
|
|
func (fs *testFileSystem) Open(name string) (File, error) {
|
http: add FileSystem interface, make FileServer use it
Permits serving from virtual filesystems, such as files linked
into a binary, or from a zip file.
Also adds a gofix for:
http.FileServer(root, prefix) -> http.StripPrefix(prefix, http.FileServer(http.Dir(root)))
R=r, rsc, gri, adg, dsymonds, r, gri
CC=golang-dev
https://golang.org/cl/4629047
2011-06-27 15:26:36 -07:00
|
|
|
return fs.open(name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestFileServerCleans(t *testing.T) {
|
2013-03-15 15:09:17 -07:00
|
|
|
defer afterTest(t)
|
http: add FileSystem interface, make FileServer use it
Permits serving from virtual filesystems, such as files linked
into a binary, or from a zip file.
Also adds a gofix for:
http.FileServer(root, prefix) -> http.StripPrefix(prefix, http.FileServer(http.Dir(root)))
R=r, rsc, gri, adg, dsymonds, r, gri
CC=golang-dev
https://golang.org/cl/4629047
2011-06-27 15:26:36 -07:00
|
|
|
ch := make(chan string, 1)
|
2011-11-01 22:04:37 -04:00
|
|
|
fs := FileServer(&testFileSystem{func(name string) (File, error) {
|
http: add FileSystem interface, make FileServer use it
Permits serving from virtual filesystems, such as files linked
into a binary, or from a zip file.
Also adds a gofix for:
http.FileServer(root, prefix) -> http.StripPrefix(prefix, http.FileServer(http.Dir(root)))
R=r, rsc, gri, adg, dsymonds, r, gri
CC=golang-dev
https://golang.org/cl/4629047
2011-06-27 15:26:36 -07:00
|
|
|
ch <- name
|
2012-02-17 10:04:29 +11:00
|
|
|
return nil, errors.New("file does not exist")
|
http: add FileSystem interface, make FileServer use it
Permits serving from virtual filesystems, such as files linked
into a binary, or from a zip file.
Also adds a gofix for:
http.FileServer(root, prefix) -> http.StripPrefix(prefix, http.FileServer(http.Dir(root)))
R=r, rsc, gri, adg, dsymonds, r, gri
CC=golang-dev
https://golang.org/cl/4629047
2011-06-27 15:26:36 -07:00
|
|
|
}})
|
|
|
|
|
tests := []struct {
|
|
|
|
|
reqPath, openArg string
|
|
|
|
|
}{
|
|
|
|
|
{"/foo.txt", "/foo.txt"},
|
|
|
|
|
{"//foo.txt", "/foo.txt"},
|
|
|
|
|
{"/../foo.txt", "/foo.txt"},
|
|
|
|
|
}
|
|
|
|
|
req, _ := NewRequest("GET", "http://example.com", nil)
|
|
|
|
|
for n, test := range tests {
|
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
|
req.URL.Path = test.reqPath
|
|
|
|
|
fs.ServeHTTP(rec, req)
|
|
|
|
|
if got := <-ch; got != test.openArg {
|
|
|
|
|
t.Errorf("test %d: got %q, want %q", n, got, test.openArg)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
net/http: escape contents of the directory indexes generated by FileServer
Previously, filenames containing special characters could:
1) Escape the <a> tag, with a file called something like: ">foo
2) Break the links in the index by prematurely ending the path portion
of the url, with a file called: foo?bar
In order to avoid a forbidden dependency on the html package, I'm
using htmlReplacer from net/http/server.go, which is equivalent to
html.EscapeString.
This change also expands fakeFile.Readdir to better emulate
os.File.Readdir.
R=golang-codereviews, rsc, gobot, bradfitz, josharian, mikioh.mikioh
CC=golang-codereviews
https://golang.org/cl/37440043
2014-01-14 12:55:12 -08:00
|
|
|
func TestFileServerEscapesNames(t *testing.T) {
|
|
|
|
|
defer afterTest(t)
|
|
|
|
|
const dirListPrefix = "<pre>\n"
|
|
|
|
|
const dirListSuffix = "\n</pre>\n"
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name, escaped string
|
|
|
|
|
}{
|
2016-09-21 19:03:06 -07:00
|
|
|
{`simple_name`, `<a href="simple_name">simple_name</a>`},
|
|
|
|
|
{`"'<>&`, `<a href="%22%27%3C%3E&">"'<>&</a>`},
|
|
|
|
|
{`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`},
|
|
|
|
|
{`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo"><combo>?foo</a>`},
|
|
|
|
|
{`foo:bar`, `<a href="./foo:bar">foo:bar</a>`},
|
net/http: escape contents of the directory indexes generated by FileServer
Previously, filenames containing special characters could:
1) Escape the <a> tag, with a file called something like: ">foo
2) Break the links in the index by prematurely ending the path portion
of the url, with a file called: foo?bar
In order to avoid a forbidden dependency on the html package, I'm
using htmlReplacer from net/http/server.go, which is equivalent to
html.EscapeString.
This change also expands fakeFile.Readdir to better emulate
os.File.Readdir.
R=golang-codereviews, rsc, gobot, bradfitz, josharian, mikioh.mikioh
CC=golang-codereviews
https://golang.org/cl/37440043
2014-01-14 12:55:12 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We put each test file in its own directory in the fakeFS so we can look at it in isolation.
|
|
|
|
|
fs := make(fakeFS)
|
|
|
|
|
for i, test := range tests {
|
|
|
|
|
testFile := &fakeFileInfo{basename: test.name}
|
|
|
|
|
fs[fmt.Sprintf("/%d", i)] = &fakeFileInfo{
|
|
|
|
|
dir: true,
|
|
|
|
|
modtime: time.Unix(1000000000, 0).UTC(),
|
|
|
|
|
ents: []*fakeFileInfo{testFile},
|
|
|
|
|
}
|
|
|
|
|
fs[fmt.Sprintf("/%d/%s", i, test.name)] = testFile
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ts := httptest.NewServer(FileServer(&fs))
|
|
|
|
|
defer ts.Close()
|
|
|
|
|
for i, test := range tests {
|
|
|
|
|
url := fmt.Sprintf("%s/%d", ts.URL, i)
|
|
|
|
|
res, err := Get(url)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("test %q: Get: %v", test.name, err)
|
|
|
|
|
}
|
|
|
|
|
b, err := ioutil.ReadAll(res.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("test %q: read Body: %v", test.name, err)
|
|
|
|
|
}
|
|
|
|
|
s := string(b)
|
|
|
|
|
if !strings.HasPrefix(s, dirListPrefix) || !strings.HasSuffix(s, dirListSuffix) {
|
|
|
|
|
t.Errorf("test %q: listing dir, full output is %q, want prefix %q and suffix %q", test.name, s, dirListPrefix, dirListSuffix)
|
|
|
|
|
}
|
|
|
|
|
if trimmed := strings.TrimSuffix(strings.TrimPrefix(s, dirListPrefix), dirListSuffix); trimmed != test.escaped {
|
|
|
|
|
t.Errorf("test %q: listing dir, filename escaped to %q, want %q", test.name, trimmed, test.escaped)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-09-01 14:44:26 -03:00
|
|
|
func TestFileServerSortsNames(t *testing.T) {
|
|
|
|
|
defer afterTest(t)
|
|
|
|
|
const contents = "I am a fake file"
|
|
|
|
|
dirMod := time.Unix(123, 0).UTC()
|
|
|
|
|
fileMod := time.Unix(1000000000, 0).UTC()
|
|
|
|
|
fs := fakeFS{
|
|
|
|
|
"/": &fakeFileInfo{
|
|
|
|
|
dir: true,
|
|
|
|
|
modtime: dirMod,
|
|
|
|
|
ents: []*fakeFileInfo{
|
|
|
|
|
{
|
|
|
|
|
basename: "b",
|
|
|
|
|
modtime: fileMod,
|
|
|
|
|
contents: contents,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
basename: "a",
|
|
|
|
|
modtime: fileMod,
|
|
|
|
|
contents: contents,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ts := httptest.NewServer(FileServer(&fs))
|
|
|
|
|
defer ts.Close()
|
|
|
|
|
|
|
|
|
|
res, err := Get(ts.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Get: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
|
|
b, err := ioutil.ReadAll(res.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("read Body: %v", err)
|
|
|
|
|
}
|
|
|
|
|
s := string(b)
|
2016-09-21 19:03:06 -07:00
|
|
|
if !strings.Contains(s, "<a href=\"a\">a</a>\n<a href=\"b\">b</a>") {
|
2015-09-01 14:44:26 -03:00
|
|
|
t.Errorf("output appears to be unsorted:\n%s", s)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2012-02-29 09:53:20 -08:00
|
|
|
func mustRemoveAll(dir string) {
|
|
|
|
|
err := os.RemoveAll(dir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2011-07-18 09:04:48 -07:00
|
|
|
func TestFileServerImplicitLeadingSlash(t *testing.T) {
|
2013-03-15 15:09:17 -07:00
|
|
|
defer afterTest(t)
|
2011-07-18 09:04:48 -07:00
|
|
|
tempDir, err := ioutil.TempDir("", "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("TempDir: %v", err)
|
|
|
|
|
}
|
2012-02-29 09:53:20 -08:00
|
|
|
defer mustRemoveAll(tempDir)
|
2011-07-18 09:04:48 -07:00
|
|
|
if err := ioutil.WriteFile(filepath.Join(tempDir, "foo.txt"), []byte("Hello world"), 0644); err != nil {
|
|
|
|
|
t.Fatalf("WriteFile: %v", err)
|
|
|
|
|
}
|
|
|
|
|
ts := httptest.NewServer(StripPrefix("/bar/", FileServer(Dir(tempDir))))
|
|
|
|
|
defer ts.Close()
|
|
|
|
|
get := func(suffix string) string {
|
|
|
|
|
res, err := Get(ts.URL + suffix)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Get %s: %v", suffix, err)
|
|
|
|
|
}
|
|
|
|
|
b, err := ioutil.ReadAll(res.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ReadAll %s: %v", suffix, err)
|
|
|
|
|
}
|
2012-02-29 09:53:20 -08:00
|
|
|
res.Body.Close()
|
2011-07-18 09:04:48 -07:00
|
|
|
return string(b)
|
|
|
|
|
}
|
|
|
|
|
if s := get("/bar/"); !strings.Contains(s, ">foo.txt<") {
|
|
|
|
|
t.Logf("expected a directory listing with foo.txt, got %q", s)
|
|
|
|
|
}
|
|
|
|
|
if s := get("/bar/foo.txt"); s != "Hello world" {
|
|
|
|
|
t.Logf("expected %q, got %q", "Hello world", s)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
http: add FileSystem interface, make FileServer use it
Permits serving from virtual filesystems, such as files linked
into a binary, or from a zip file.
Also adds a gofix for:
http.FileServer(root, prefix) -> http.StripPrefix(prefix, http.FileServer(http.Dir(root)))
R=r, rsc, gri, adg, dsymonds, r, gri
CC=golang-dev
https://golang.org/cl/4629047
2011-06-27 15:26:36 -07:00
|
|
|
func TestDirJoin(t *testing.T) {
|
2013-07-30 18:25:08 -07:00
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
t.Skip("skipping test on windows")
|
|
|
|
|
}
|
http: add FileSystem interface, make FileServer use it
Permits serving from virtual filesystems, such as files linked
into a binary, or from a zip file.
Also adds a gofix for:
http.FileServer(root, prefix) -> http.StripPrefix(prefix, http.FileServer(http.Dir(root)))
R=r, rsc, gri, adg, dsymonds, r, gri
CC=golang-dev
https://golang.org/cl/4629047
2011-06-27 15:26:36 -07:00
|
|
|
wfi, err := os.Stat("/etc/hosts")
|
|
|
|
|
if err != nil {
|
2013-01-24 17:32:10 +11:00
|
|
|
t.Skip("skipping test; no /etc/hosts file")
|
http: add FileSystem interface, make FileServer use it
Permits serving from virtual filesystems, such as files linked
into a binary, or from a zip file.
Also adds a gofix for:
http.FileServer(root, prefix) -> http.StripPrefix(prefix, http.FileServer(http.Dir(root)))
R=r, rsc, gri, adg, dsymonds, r, gri
CC=golang-dev
https://golang.org/cl/4629047
2011-06-27 15:26:36 -07:00
|
|
|
}
|
|
|
|
|
test := func(d Dir, name string) {
|
|
|
|
|
f, err := d.Open(name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("open of %s: %v", name, err)
|
|
|
|
|
}
|
|
|
|
|
defer f.Close()
|
|
|
|
|
gfi, err := f.Stat()
|
|
|
|
|
if err != nil {
|
2011-07-22 17:11:44 +10:00
|
|
|
t.Fatalf("stat of %s: %v", name, err)
|
http: add FileSystem interface, make FileServer use it
Permits serving from virtual filesystems, such as files linked
into a binary, or from a zip file.
Also adds a gofix for:
http.FileServer(root, prefix) -> http.StripPrefix(prefix, http.FileServer(http.Dir(root)))
R=r, rsc, gri, adg, dsymonds, r, gri
CC=golang-dev
https://golang.org/cl/4629047
2011-06-27 15:26:36 -07:00
|
|
|
}
|
2012-02-03 00:16:18 -02:00
|
|
|
if !os.SameFile(gfi, wfi) {
|
2011-11-30 12:04:16 -05:00
|
|
|
t.Errorf("%s got different file", name)
|
http: add FileSystem interface, make FileServer use it
Permits serving from virtual filesystems, such as files linked
into a binary, or from a zip file.
Also adds a gofix for:
http.FileServer(root, prefix) -> http.StripPrefix(prefix, http.FileServer(http.Dir(root)))
R=r, rsc, gri, adg, dsymonds, r, gri
CC=golang-dev
https://golang.org/cl/4629047
2011-06-27 15:26:36 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
test(Dir("/etc/"), "/hosts")
|
|
|
|
|
test(Dir("/etc/"), "hosts")
|
|
|
|
|
test(Dir("/etc/"), "../../../../hosts")
|
|
|
|
|
test(Dir("/etc"), "/hosts")
|
|
|
|
|
test(Dir("/etc"), "hosts")
|
|
|
|
|
test(Dir("/etc"), "../../../../hosts")
|
|
|
|
|
|
|
|
|
|
// Not really directories, but since we use this trick in
|
|
|
|
|
// ServeFile, test it:
|
|
|
|
|
test(Dir("/etc/hosts"), "")
|
|
|
|
|
test(Dir("/etc/hosts"), "/")
|
|
|
|
|
test(Dir("/etc/hosts"), "../")
|
|
|
|
|
}
|
|
|
|
|
|
2011-11-17 11:42:25 +11:00
|
|
|
func TestEmptyDirOpenCWD(t *testing.T) {
|
|
|
|
|
test := func(d Dir) {
|
|
|
|
|
name := "fs_test.go"
|
|
|
|
|
f, err := d.Open(name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("open of %s: %v", name, err)
|
|
|
|
|
}
|
|
|
|
|
defer f.Close()
|
|
|
|
|
}
|
|
|
|
|
test(Dir(""))
|
|
|
|
|
test(Dir("."))
|
|
|
|
|
test(Dir("./"))
|
|
|
|
|
}
|
|
|
|
|
|
2011-04-06 14:52:42 +10:00
|
|
|
func TestServeFileContentType(t *testing.T) {
|
2013-03-15 15:09:17 -07:00
|
|
|
defer afterTest(t)
|
2011-04-06 14:52:42 +10:00
|
|
|
const ctype = "icecream/chocolate"
|
|
|
|
|
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
|
2013-10-15 08:22:04 +11:00
|
|
|
switch r.FormValue("override") {
|
|
|
|
|
case "1":
|
2011-04-06 14:52:42 +10:00
|
|
|
w.Header().Set("Content-Type", ctype)
|
2013-10-15 08:22:04 +11:00
|
|
|
case "2":
|
|
|
|
|
// Explicitly inhibit sniffing.
|
|
|
|
|
w.Header()["Content-Type"] = []string{}
|
2011-04-06 14:52:42 +10:00
|
|
|
}
|
|
|
|
|
ServeFile(w, r, "testdata/file")
|
|
|
|
|
}))
|
|
|
|
|
defer ts.Close()
|
2013-10-15 08:22:04 +11:00
|
|
|
get := func(override string, want []string) {
|
2012-01-18 08:28:09 +11:00
|
|
|
resp, err := Get(ts.URL + "?override=" + override)
|
2011-04-06 14:52:42 +10:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2013-10-15 08:22:04 +11:00
|
|
|
if h := resp.Header["Content-Type"]; !reflect.DeepEqual(h, want) {
|
|
|
|
|
t.Errorf("Content-Type mismatch: got %v, want %v", h, want)
|
2011-04-06 14:52:42 +10:00
|
|
|
}
|
2013-02-26 17:12:50 -08:00
|
|
|
resp.Body.Close()
|
2011-04-06 14:52:42 +10:00
|
|
|
}
|
2013-10-15 08:22:04 +11:00
|
|
|
get("0", []string{"text/plain; charset=utf-8"})
|
|
|
|
|
get("1", []string{ctype})
|
|
|
|
|
get("2", nil)
|
2011-04-06 14:52:42 +10:00
|
|
|
}
|
|
|
|
|
|
2011-07-13 14:39:33 -07:00
|
|
|
func TestServeFileMimeType(t *testing.T) {
|
2013-03-15 15:09:17 -07:00
|
|
|
defer afterTest(t)
|
2011-07-13 14:39:33 -07:00
|
|
|
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
|
|
|
|
|
ServeFile(w, r, "testdata/style.css")
|
|
|
|
|
}))
|
|
|
|
|
defer ts.Close()
|
|
|
|
|
resp, err := Get(ts.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2013-02-26 17:12:50 -08:00
|
|
|
resp.Body.Close()
|
2011-07-13 15:48:57 -07:00
|
|
|
want := "text/css; charset=utf-8"
|
2011-07-13 14:39:33 -07:00
|
|
|
if h := resp.Header.Get("Content-Type"); h != want {
|
|
|
|
|
t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2011-11-17 11:42:25 +11:00
|
|
|
func TestServeFileFromCWD(t *testing.T) {
|
2013-03-15 15:09:17 -07:00
|
|
|
defer afterTest(t)
|
2011-11-17 11:42:25 +11:00
|
|
|
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
|
|
|
|
|
ServeFile(w, r, "fs_test.go")
|
|
|
|
|
}))
|
|
|
|
|
defer ts.Close()
|
|
|
|
|
r, err := Get(ts.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2012-09-24 12:48:19 +10:00
|
|
|
r.Body.Close()
|
2011-11-17 11:42:25 +11:00
|
|
|
if r.StatusCode != 200 {
|
|
|
|
|
t.Fatalf("expected 200 OK, got %s", r.Status)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-03 23:53:39 +05:30
|
|
|
// Issue 13996
|
|
|
|
|
func TestServeDirWithoutTrailingSlash(t *testing.T) {
|
|
|
|
|
e := "/testdata/"
|
|
|
|
|
defer afterTest(t)
|
|
|
|
|
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
|
|
|
|
|
ServeFile(w, r, ".")
|
|
|
|
|
}))
|
|
|
|
|
defer ts.Close()
|
|
|
|
|
r, err := Get(ts.URL + "/testdata")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
r.Body.Close()
|
|
|
|
|
if g := r.Request.URL.Path; g != e {
|
|
|
|
|
t.Errorf("got %s, want %s", g, e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-12-09 22:02:46 +00:00
|
|
|
// Tests that ServeFile doesn't add a Content-Length if a Content-Encoding is
|
|
|
|
|
// specified.
|
|
|
|
|
func TestServeFileWithContentEncoding_h1(t *testing.T) { testServeFileWithContentEncoding(t, h1Mode) }
|
|
|
|
|
func TestServeFileWithContentEncoding_h2(t *testing.T) { testServeFileWithContentEncoding(t, h2Mode) }
|
|
|
|
|
func testServeFileWithContentEncoding(t *testing.T, h2 bool) {
|
2013-03-15 15:09:17 -07:00
|
|
|
defer afterTest(t)
|
2015-12-09 22:02:46 +00:00
|
|
|
cst := newClientServerTest(t, h2, HandlerFunc(func(w ResponseWriter, r *Request) {
|
2011-06-02 13:36:52 -07:00
|
|
|
w.Header().Set("Content-Encoding", "foo")
|
|
|
|
|
ServeFile(w, r, "testdata/file")
|
2015-12-09 22:02:46 +00:00
|
|
|
|
|
|
|
|
// Because the testdata is so small, it would fit in
|
|
|
|
|
// both the h1 and h2 Server's write buffers. For h1,
|
|
|
|
|
// sendfile is used, though, forcing a header flush at
|
|
|
|
|
// the io.Copy. http2 doesn't do a header flush so
|
|
|
|
|
// buffers all 11 bytes and then adds its own
|
|
|
|
|
// Content-Length. To prevent the Server's
|
|
|
|
|
// Content-Length and test ServeFile only, flush here.
|
|
|
|
|
w.(Flusher).Flush()
|
2011-06-02 13:36:52 -07:00
|
|
|
}))
|
2015-12-09 22:02:46 +00:00
|
|
|
defer cst.close()
|
|
|
|
|
resp, err := cst.c.Get(cst.ts.URL)
|
2011-06-02 13:36:52 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2013-02-26 17:12:50 -08:00
|
|
|
resp.Body.Close()
|
2011-06-02 13:36:52 -07:00
|
|
|
if g, e := resp.ContentLength, int64(-1); g != e {
|
2011-08-01 11:50:50 +10:00
|
|
|
t.Errorf("Content-Length mismatch: got %d, want %d", g, e)
|
2011-06-02 13:36:52 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2011-08-09 10:25:53 -07:00
|
|
|
func TestServeIndexHtml(t *testing.T) {
|
2013-03-15 15:09:17 -07:00
|
|
|
defer afterTest(t)
|
2011-08-09 10:25:53 -07:00
|
|
|
const want = "index.html says hello\n"
|
|
|
|
|
ts := httptest.NewServer(FileServer(Dir(".")))
|
|
|
|
|
defer ts.Close()
|
|
|
|
|
|
|
|
|
|
for _, path := range []string{"/testdata/", "/testdata/index.html"} {
|
|
|
|
|
res, err := Get(ts.URL + path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
b, err := ioutil.ReadAll(res.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal("reading Body:", err)
|
|
|
|
|
}
|
|
|
|
|
if s := string(b); s != want {
|
|
|
|
|
t.Errorf("for path %q got %q, want %q", path, s, want)
|
|
|
|
|
}
|
2012-02-10 10:02:06 +11:00
|
|
|
res.Body.Close()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2012-07-30 13:57:30 +10:00
|
|
|
func TestFileServerZeroByte(t *testing.T) {
|
2013-03-15 15:09:17 -07:00
|
|
|
defer afterTest(t)
|
2012-07-30 13:57:30 +10:00
|
|
|
ts := httptest.NewServer(FileServer(Dir(".")))
|
|
|
|
|
defer ts.Close()
|
|
|
|
|
|
|
|
|
|
res, err := Get(ts.URL + "/..\x00")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
b, err := ioutil.ReadAll(res.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal("reading Body:", err)
|
|
|
|
|
}
|
|
|
|
|
if res.StatusCode == 200 {
|
|
|
|
|
t.Errorf("got status 200; want an error. Body is:\n%s", string(b))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2012-06-13 14:53:05 -07:00
|
|
|
type fakeFileInfo struct {
|
|
|
|
|
dir bool
|
|
|
|
|
basename string
|
|
|
|
|
modtime time.Time
|
|
|
|
|
ents []*fakeFileInfo
|
|
|
|
|
contents string
|
2015-04-21 13:36:44 -07:00
|
|
|
err error
|
2012-06-13 14:53:05 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (f *fakeFileInfo) Name() string { return f.basename }
|
|
|
|
|
func (f *fakeFileInfo) Sys() interface{} { return nil }
|
|
|
|
|
func (f *fakeFileInfo) ModTime() time.Time { return f.modtime }
|
|
|
|
|
func (f *fakeFileInfo) IsDir() bool { return f.dir }
|
|
|
|
|
func (f *fakeFileInfo) Size() int64 { return int64(len(f.contents)) }
|
|
|
|
|
func (f *fakeFileInfo) Mode() os.FileMode {
|
|
|
|
|
if f.dir {
|
|
|
|
|
return 0755 | os.ModeDir
|
|
|
|
|
}
|
|
|
|
|
return 0644
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type fakeFile struct {
|
|
|
|
|
io.ReadSeeker
|
net/http: escape contents of the directory indexes generated by FileServer
Previously, filenames containing special characters could:
1) Escape the <a> tag, with a file called something like: ">foo
2) Break the links in the index by prematurely ending the path portion
of the url, with a file called: foo?bar
In order to avoid a forbidden dependency on the html package, I'm
using htmlReplacer from net/http/server.go, which is equivalent to
html.EscapeString.
This change also expands fakeFile.Readdir to better emulate
os.File.Readdir.
R=golang-codereviews, rsc, gobot, bradfitz, josharian, mikioh.mikioh
CC=golang-codereviews
https://golang.org/cl/37440043
2014-01-14 12:55:12 -08:00
|
|
|
fi *fakeFileInfo
|
|
|
|
|
path string // as opened
|
|
|
|
|
entpos int
|
2012-06-13 14:53:05 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (f *fakeFile) Close() error { return nil }
|
|
|
|
|
func (f *fakeFile) Stat() (os.FileInfo, error) { return f.fi, nil }
|
|
|
|
|
func (f *fakeFile) Readdir(count int) ([]os.FileInfo, error) {
|
|
|
|
|
if !f.fi.dir {
|
|
|
|
|
return nil, os.ErrInvalid
|
|
|
|
|
}
|
|
|
|
|
var fis []os.FileInfo
|
net/http: escape contents of the directory indexes generated by FileServer
Previously, filenames containing special characters could:
1) Escape the <a> tag, with a file called something like: ">foo
2) Break the links in the index by prematurely ending the path portion
of the url, with a file called: foo?bar
In order to avoid a forbidden dependency on the html package, I'm
using htmlReplacer from net/http/server.go, which is equivalent to
html.EscapeString.
This change also expands fakeFile.Readdir to better emulate
os.File.Readdir.
R=golang-codereviews, rsc, gobot, bradfitz, josharian, mikioh.mikioh
CC=golang-codereviews
https://golang.org/cl/37440043
2014-01-14 12:55:12 -08:00
|
|
|
|
|
|
|
|
limit := f.entpos + count
|
|
|
|
|
if count <= 0 || limit > len(f.fi.ents) {
|
|
|
|
|
limit = len(f.fi.ents)
|
|
|
|
|
}
|
|
|
|
|
for ; f.entpos < limit; f.entpos++ {
|
|
|
|
|
fis = append(fis, f.fi.ents[f.entpos])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(fis) == 0 && count > 0 {
|
|
|
|
|
return fis, io.EOF
|
|
|
|
|
} else {
|
|
|
|
|
return fis, nil
|
2012-06-13 14:53:05 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type fakeFS map[string]*fakeFileInfo
|
|
|
|
|
|
|
|
|
|
func (fs fakeFS) Open(name string) (File, error) {
|
|
|
|
|
name = path.Clean(name)
|
|
|
|
|
f, ok := fs[name]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, os.ErrNotExist
|
|
|
|
|
}
|
2015-04-21 13:36:44 -07:00
|
|
|
if f.err != nil {
|
|
|
|
|
return nil, f.err
|
|
|
|
|
}
|
2012-06-13 14:53:05 -07:00
|
|
|
return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDirectoryIfNotModified(t *testing.T) {
|
2013-03-15 15:09:17 -07:00
|
|
|
defer afterTest(t)
|
2012-06-13 14:53:05 -07:00
|
|
|
const indexContents = "I am a fake index.html file"
|
|
|
|
|
fileMod := time.Unix(1000000000, 0).UTC()
|
|
|
|
|
fileModStr := fileMod.Format(TimeFormat)
|
|
|
|
|
dirMod := time.Unix(123, 0).UTC()
|
|
|
|
|
indexFile := &fakeFileInfo{
|
|
|
|
|
basename: "index.html",
|
|
|
|
|
modtime: fileMod,
|
|
|
|
|
contents: indexContents,
|
|
|
|
|
}
|
|
|
|
|
fs := fakeFS{
|
|
|
|
|
"/": &fakeFileInfo{
|
|
|
|
|
dir: true,
|
|
|
|
|
modtime: dirMod,
|
|
|
|
|
ents: []*fakeFileInfo{indexFile},
|
|
|
|
|
},
|
|
|
|
|
"/index.html": indexFile,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ts := httptest.NewServer(FileServer(fs))
|
|
|
|
|
defer ts.Close()
|
|
|
|
|
|
|
|
|
|
res, err := Get(ts.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
b, err := ioutil.ReadAll(res.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if string(b) != indexContents {
|
|
|
|
|
t.Fatalf("Got body %q; want %q", b, indexContents)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
lastMod := res.Header.Get("Last-Modified")
|
|
|
|
|
if lastMod != fileModStr {
|
|
|
|
|
t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req, _ := NewRequest("GET", ts.URL, nil)
|
|
|
|
|
req.Header.Set("If-Modified-Since", lastMod)
|
|
|
|
|
|
2017-03-04 18:24:44 +00:00
|
|
|
c := ts.Client()
|
|
|
|
|
res, err = c.Do(req)
|
2012-06-13 14:53:05 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if res.StatusCode != 304 {
|
|
|
|
|
t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
// Advance the index.html file's modtime, but not the directory's.
|
|
|
|
|
indexFile.modtime = indexFile.modtime.Add(1 * time.Hour)
|
|
|
|
|
|
2017-03-04 18:24:44 +00:00
|
|
|
res, err = c.Do(req)
|
2012-06-13 14:53:05 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if res.StatusCode != 200 {
|
|
|
|
|
t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
}
|
|
|
|
|
|
2012-09-10 10:16:09 -07:00
|
|
|
func mustStat(t *testing.T, fileName string) os.FileInfo {
|
|
|
|
|
fi, err := os.Stat(fileName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
2012-02-10 10:02:06 +11:00
|
|
|
}
|
2012-09-10 10:16:09 -07:00
|
|
|
return fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestServeContent(t *testing.T) {
|
2013-03-15 15:09:17 -07:00
|
|
|
defer afterTest(t)
|
2012-09-10 10:16:09 -07:00
|
|
|
type serveParam struct {
|
|
|
|
|
name string
|
|
|
|
|
modtime time.Time
|
|
|
|
|
content io.ReadSeeker
|
|
|
|
|
contentType string
|
|
|
|
|
etag string
|
|
|
|
|
}
|
|
|
|
|
servec := make(chan serveParam, 1)
|
2012-02-10 10:02:06 +11:00
|
|
|
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
|
2012-09-10 10:16:09 -07:00
|
|
|
p := <-servec
|
|
|
|
|
if p.etag != "" {
|
|
|
|
|
w.Header().Set("ETag", p.etag)
|
|
|
|
|
}
|
|
|
|
|
if p.contentType != "" {
|
|
|
|
|
w.Header().Set("Content-Type", p.contentType)
|
|
|
|
|
}
|
2012-02-10 10:02:06 +11:00
|
|
|
ServeContent(w, r, p.name, p.modtime, p.content)
|
|
|
|
|
}))
|
|
|
|
|
defer ts.Close()
|
|
|
|
|
|
2012-09-10 10:16:09 -07:00
|
|
|
type testCase struct {
|
2013-07-10 13:29:52 +10:00
|
|
|
// One of file or content must be set:
|
|
|
|
|
file string
|
|
|
|
|
content io.ReadSeeker
|
|
|
|
|
|
2012-09-10 10:16:09 -07:00
|
|
|
modtime time.Time
|
|
|
|
|
serveETag string // optional
|
|
|
|
|
serveContentType string // optional
|
|
|
|
|
reqHeader map[string]string
|
|
|
|
|
wantLastMod string
|
|
|
|
|
wantContentType string
|
2016-06-17 21:02:59 +04:30
|
|
|
wantContentRange string
|
2012-09-10 10:16:09 -07:00
|
|
|
wantStatus int
|
|
|
|
|
}
|
|
|
|
|
htmlModTime := mustStat(t, "testdata/index.html").ModTime()
|
|
|
|
|
tests := map[string]testCase{
|
|
|
|
|
"no_last_modified": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
wantContentType: "text/css; charset=utf-8",
|
|
|
|
|
wantStatus: 200,
|
|
|
|
|
},
|
|
|
|
|
"with_last_modified": {
|
|
|
|
|
file: "testdata/index.html",
|
|
|
|
|
wantContentType: "text/html; charset=utf-8",
|
|
|
|
|
modtime: htmlModTime,
|
|
|
|
|
wantLastMod: htmlModTime.UTC().Format(TimeFormat),
|
|
|
|
|
wantStatus: 200,
|
|
|
|
|
},
|
|
|
|
|
"not_modified_modtime": {
|
2016-10-25 12:51:39 -07:00
|
|
|
file: "testdata/style.css",
|
|
|
|
|
serveETag: `"foo"`, // Last-Modified sent only when no ETag
|
|
|
|
|
modtime: htmlModTime,
|
2012-09-10 10:16:09 -07:00
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
|
|
|
|
|
},
|
|
|
|
|
wantStatus: 304,
|
|
|
|
|
},
|
|
|
|
|
"not_modified_modtime_with_contenttype": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
serveContentType: "text/css", // explicit content type
|
2016-10-25 12:51:39 -07:00
|
|
|
serveETag: `"foo"`, // Last-Modified sent only when no ETag
|
2012-09-10 10:16:09 -07:00
|
|
|
modtime: htmlModTime,
|
|
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
|
|
|
|
|
},
|
|
|
|
|
wantStatus: 304,
|
|
|
|
|
},
|
|
|
|
|
"not_modified_etag": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
serveETag: `"foo"`,
|
|
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"If-None-Match": `"foo"`,
|
|
|
|
|
},
|
|
|
|
|
wantStatus: 304,
|
|
|
|
|
},
|
2013-07-10 13:29:52 +10:00
|
|
|
"not_modified_etag_no_seek": {
|
|
|
|
|
content: panicOnSeek{nil}, // should never be called
|
2016-10-25 12:51:39 -07:00
|
|
|
serveETag: `W/"foo"`, // If-None-Match uses weak ETag comparison
|
2013-07-10 13:29:52 +10:00
|
|
|
reqHeader: map[string]string{
|
2016-10-25 12:51:39 -07:00
|
|
|
"If-None-Match": `"baz", W/"foo"`,
|
2013-07-10 13:29:52 +10:00
|
|
|
},
|
|
|
|
|
wantStatus: 304,
|
|
|
|
|
},
|
2016-10-25 12:51:39 -07:00
|
|
|
"if_none_match_mismatch": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
serveETag: `"foo"`,
|
|
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"If-None-Match": `"Foo"`,
|
|
|
|
|
},
|
|
|
|
|
wantStatus: 200,
|
|
|
|
|
wantContentType: "text/css; charset=utf-8",
|
|
|
|
|
},
|
2012-09-10 10:16:09 -07:00
|
|
|
"range_good": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
serveETag: `"A"`,
|
|
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"Range": "bytes=0-4",
|
|
|
|
|
},
|
2016-06-17 21:02:59 +04:30
|
|
|
wantStatus: StatusPartialContent,
|
|
|
|
|
wantContentType: "text/css; charset=utf-8",
|
|
|
|
|
wantContentRange: "bytes 0-4/8",
|
|
|
|
|
},
|
2016-10-25 12:51:39 -07:00
|
|
|
"range_match": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
serveETag: `"A"`,
|
|
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"Range": "bytes=0-4",
|
|
|
|
|
"If-Range": `"A"`,
|
|
|
|
|
},
|
|
|
|
|
wantStatus: StatusPartialContent,
|
|
|
|
|
wantContentType: "text/css; charset=utf-8",
|
|
|
|
|
wantContentRange: "bytes 0-4/8",
|
|
|
|
|
},
|
|
|
|
|
"range_match_weak_etag": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
serveETag: `W/"A"`,
|
|
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"Range": "bytes=0-4",
|
|
|
|
|
"If-Range": `W/"A"`,
|
|
|
|
|
},
|
|
|
|
|
wantStatus: 200,
|
|
|
|
|
wantContentType: "text/css; charset=utf-8",
|
|
|
|
|
},
|
2016-06-17 21:02:59 +04:30
|
|
|
"range_no_overlap": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
serveETag: `"A"`,
|
|
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"Range": "bytes=10-20",
|
|
|
|
|
},
|
|
|
|
|
wantStatus: StatusRequestedRangeNotSatisfiable,
|
|
|
|
|
wantContentType: "text/plain; charset=utf-8",
|
|
|
|
|
wantContentRange: "bytes */8",
|
2012-09-10 10:16:09 -07:00
|
|
|
},
|
|
|
|
|
// An If-Range resource for entity "A", but entity "B" is now current.
|
|
|
|
|
// The Range request should be ignored.
|
|
|
|
|
"range_no_match": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
serveETag: `"A"`,
|
|
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"Range": "bytes=0-4",
|
|
|
|
|
"If-Range": `"B"`,
|
|
|
|
|
},
|
|
|
|
|
wantStatus: 200,
|
|
|
|
|
wantContentType: "text/css; charset=utf-8",
|
|
|
|
|
},
|
2014-07-27 23:30:53 -07:00
|
|
|
"range_with_modtime": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC),
|
|
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"Range": "bytes=0-4",
|
|
|
|
|
"If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
|
|
|
|
|
},
|
2016-06-17 21:02:59 +04:30
|
|
|
wantStatus: StatusPartialContent,
|
|
|
|
|
wantContentType: "text/css; charset=utf-8",
|
|
|
|
|
wantContentRange: "bytes 0-4/8",
|
|
|
|
|
wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
|
2014-07-27 23:30:53 -07:00
|
|
|
},
|
2017-08-09 03:26:45 +01:00
|
|
|
"range_with_modtime_mismatch": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC),
|
|
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"Range": "bytes=0-4",
|
|
|
|
|
"If-Range": "Wed, 25 Jun 2014 17:12:19 GMT",
|
|
|
|
|
},
|
|
|
|
|
wantStatus: StatusOK,
|
|
|
|
|
wantContentType: "text/css; charset=utf-8",
|
|
|
|
|
wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
|
|
|
|
|
},
|
2014-07-27 23:30:53 -07:00
|
|
|
"range_with_modtime_nanos": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
modtime: time.Date(2014, 6, 25, 17, 12, 18, 123 /* nanos */, time.UTC),
|
|
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"Range": "bytes=0-4",
|
|
|
|
|
"If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
|
|
|
|
|
},
|
2016-06-17 21:02:59 +04:30
|
|
|
wantStatus: StatusPartialContent,
|
|
|
|
|
wantContentType: "text/css; charset=utf-8",
|
|
|
|
|
wantContentRange: "bytes 0-4/8",
|
|
|
|
|
wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
|
2014-07-27 23:30:53 -07:00
|
|
|
},
|
2015-02-12 08:42:11 -08:00
|
|
|
"unix_zero_modtime": {
|
|
|
|
|
content: strings.NewReader("<html>foo"),
|
|
|
|
|
modtime: time.Unix(0, 0),
|
|
|
|
|
wantStatus: StatusOK,
|
|
|
|
|
wantContentType: "text/html; charset=utf-8",
|
|
|
|
|
},
|
2016-10-25 12:51:39 -07:00
|
|
|
"ifmatch_matches": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
serveETag: `"A"`,
|
|
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"If-Match": `"Z", "A"`,
|
|
|
|
|
},
|
|
|
|
|
wantStatus: 200,
|
|
|
|
|
wantContentType: "text/css; charset=utf-8",
|
|
|
|
|
},
|
|
|
|
|
"ifmatch_star": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
serveETag: `"A"`,
|
|
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"If-Match": `*`,
|
|
|
|
|
},
|
|
|
|
|
wantStatus: 200,
|
|
|
|
|
wantContentType: "text/css; charset=utf-8",
|
|
|
|
|
},
|
|
|
|
|
"ifmatch_failed": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
serveETag: `"A"`,
|
|
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"If-Match": `"B"`,
|
|
|
|
|
},
|
2017-06-24 14:23:17 +08:00
|
|
|
wantStatus: 412,
|
2016-10-25 12:51:39 -07:00
|
|
|
},
|
|
|
|
|
"ifmatch_fails_on_weak_etag": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
serveETag: `W/"A"`,
|
|
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"If-Match": `W/"A"`,
|
|
|
|
|
},
|
2017-06-24 14:23:17 +08:00
|
|
|
wantStatus: 412,
|
2016-10-25 12:51:39 -07:00
|
|
|
},
|
|
|
|
|
"if_unmodified_since_true": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
modtime: htmlModTime,
|
|
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"If-Unmodified-Since": htmlModTime.UTC().Format(TimeFormat),
|
|
|
|
|
},
|
|
|
|
|
wantStatus: 200,
|
|
|
|
|
wantContentType: "text/css; charset=utf-8",
|
|
|
|
|
wantLastMod: htmlModTime.UTC().Format(TimeFormat),
|
|
|
|
|
},
|
|
|
|
|
"if_unmodified_since_false": {
|
|
|
|
|
file: "testdata/style.css",
|
|
|
|
|
modtime: htmlModTime,
|
|
|
|
|
reqHeader: map[string]string{
|
|
|
|
|
"If-Unmodified-Since": htmlModTime.Add(-2 * time.Second).UTC().Format(TimeFormat),
|
|
|
|
|
},
|
2017-06-24 14:23:17 +08:00
|
|
|
wantStatus: 412,
|
|
|
|
|
wantLastMod: htmlModTime.UTC().Format(TimeFormat),
|
2016-10-25 12:51:39 -07:00
|
|
|
},
|
2012-02-10 10:02:06 +11:00
|
|
|
}
|
2012-09-10 10:16:09 -07:00
|
|
|
for testName, tt := range tests {
|
2013-07-10 13:29:52 +10:00
|
|
|
var content io.ReadSeeker
|
|
|
|
|
if tt.file != "" {
|
|
|
|
|
f, err := os.Open(tt.file)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("test %q: %v", testName, err)
|
|
|
|
|
}
|
|
|
|
|
defer f.Close()
|
|
|
|
|
content = f
|
|
|
|
|
} else {
|
|
|
|
|
content = tt.content
|
2012-09-10 10:16:09 -07:00
|
|
|
}
|
2017-08-09 03:26:45 +01:00
|
|
|
for _, method := range []string{"GET", "HEAD"} {
|
|
|
|
|
//restore content in case it is consumed by previous method
|
|
|
|
|
if content, ok := content.(*strings.Reader); ok {
|
|
|
|
|
content.Seek(io.SeekStart, 0)
|
|
|
|
|
}
|
2012-02-10 10:02:06 +11:00
|
|
|
|
2017-08-09 03:26:45 +01:00
|
|
|
servec <- serveParam{
|
|
|
|
|
name: filepath.Base(tt.file),
|
|
|
|
|
content: content,
|
|
|
|
|
modtime: tt.modtime,
|
|
|
|
|
etag: tt.serveETag,
|
|
|
|
|
contentType: tt.serveContentType,
|
|
|
|
|
}
|
|
|
|
|
req, err := NewRequest(method, ts.URL, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for k, v := range tt.reqHeader {
|
|
|
|
|
req.Header.Set(k, v)
|
|
|
|
|
}
|
2017-03-04 18:24:44 +00:00
|
|
|
|
2017-08-09 03:26:45 +01:00
|
|
|
c := ts.Client()
|
|
|
|
|
res, err := c.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
io.Copy(ioutil.Discard, res.Body)
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
if res.StatusCode != tt.wantStatus {
|
|
|
|
|
t.Errorf("test %q using %q: got status = %d; want %d", testName, method, res.StatusCode, tt.wantStatus)
|
|
|
|
|
}
|
|
|
|
|
if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e {
|
|
|
|
|
t.Errorf("test %q using %q: got content-type = %q, want %q", testName, method, g, e)
|
|
|
|
|
}
|
|
|
|
|
if g, e := res.Header.Get("Content-Range"), tt.wantContentRange; g != e {
|
|
|
|
|
t.Errorf("test %q using %q: got content-range = %q, want %q", testName, method, g, e)
|
|
|
|
|
}
|
|
|
|
|
if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e {
|
|
|
|
|
t.Errorf("test %q using %q: got last-modified = %q, want %q", testName, method, g, e)
|
|
|
|
|
}
|
2012-09-10 10:16:09 -07:00
|
|
|
}
|
2011-08-09 10:25:53 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-10-19 14:45:50 +00:00
|
|
|
// Issue 12991
|
|
|
|
|
func TestServerFileStatError(t *testing.T) {
|
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
|
r, _ := NewRequest("GET", "http://foo/", nil)
|
|
|
|
|
redirect := false
|
|
|
|
|
name := "file.txt"
|
|
|
|
|
fs := issue12991FS{}
|
|
|
|
|
ExportServeFile(rec, r, fs, name, redirect)
|
|
|
|
|
if body := rec.Body.String(); !strings.Contains(body, "403") || !strings.Contains(body, "Forbidden") {
|
|
|
|
|
t.Errorf("wanted 403 forbidden message; got: %s", body)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type issue12991FS struct{}
|
|
|
|
|
|
|
|
|
|
func (issue12991FS) Open(string) (File, error) { return issue12991File{}, nil }
|
|
|
|
|
|
|
|
|
|
type issue12991File struct{ File }
|
|
|
|
|
|
|
|
|
|
func (issue12991File) Stat() (os.FileInfo, error) { return nil, os.ErrPermission }
|
|
|
|
|
func (issue12991File) Close() error { return nil }
|
|
|
|
|
|
2015-04-21 13:36:44 -07:00
|
|
|
func TestServeContentErrorMessages(t *testing.T) {
|
|
|
|
|
defer afterTest(t)
|
|
|
|
|
fs := fakeFS{
|
|
|
|
|
"/500": &fakeFileInfo{
|
|
|
|
|
err: errors.New("random error"),
|
|
|
|
|
},
|
|
|
|
|
"/403": &fakeFileInfo{
|
|
|
|
|
err: &os.PathError{Err: os.ErrPermission},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
ts := httptest.NewServer(FileServer(fs))
|
|
|
|
|
defer ts.Close()
|
2017-03-04 18:24:44 +00:00
|
|
|
c := ts.Client()
|
2015-04-21 13:36:44 -07:00
|
|
|
for _, code := range []int{403, 404, 500} {
|
2017-03-04 18:24:44 +00:00
|
|
|
res, err := c.Get(fmt.Sprintf("%s/%d", ts.URL, code))
|
2015-04-21 13:36:44 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("Error fetching /%d: %v", code, err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if res.StatusCode != code {
|
|
|
|
|
t.Errorf("For /%d, status code = %d; want %d", code, res.StatusCode, code)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2012-02-14 09:34:52 +11:00
|
|
|
// verifies that sendfile is being used on Linux
|
|
|
|
|
func TestLinuxSendfile(t *testing.T) {
|
2016-11-04 03:23:37 +00:00
|
|
|
setParallel(t)
|
2013-03-15 15:09:17 -07:00
|
|
|
defer afterTest(t)
|
2012-02-14 09:34:52 +11:00
|
|
|
if runtime.GOOS != "linux" {
|
2013-01-24 17:32:10 +11:00
|
|
|
t.Skip("skipping; linux-only test")
|
2012-02-14 09:34:52 +11:00
|
|
|
}
|
2013-01-24 17:32:10 +11:00
|
|
|
if _, err := exec.LookPath("strace"); err != nil {
|
|
|
|
|
t.Skip("skipping; strace not found in path")
|
2012-02-14 09:34:52 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
lnf, err := ln.(*net.TCPListener).File()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
defer ln.Close()
|
|
|
|
|
|
2015-09-10 11:33:31 -04:00
|
|
|
syscalls := "sendfile,sendfile64"
|
|
|
|
|
switch runtime.GOARCH {
|
2017-06-06 16:13:39 +02:00
|
|
|
case "mips64", "mips64le", "s390x":
|
2016-04-12 12:46:54 -04:00
|
|
|
// strace on the above platforms doesn't support sendfile64
|
|
|
|
|
// and will error out if we specify that with `-e trace='.
|
2015-09-10 11:33:31 -04:00
|
|
|
syscalls = "sendfile"
|
2017-03-17 23:03:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Attempt to run strace, and skip on failure - this test requires SYS_PTRACE.
|
|
|
|
|
if err := exec.Command("strace", "-f", "-q", "-e", "trace="+syscalls, os.Args[0], "-test.run=^$").Run(); err != nil {
|
|
|
|
|
t.Skipf("skipping; failed to run strace: %v", err)
|
2015-09-10 11:33:31 -04:00
|
|
|
}
|
|
|
|
|
|
2012-02-15 11:05:51 +11:00
|
|
|
var buf bytes.Buffer
|
2015-09-10 11:33:31 -04:00
|
|
|
child := exec.Command("strace", "-f", "-q", "-e", "trace="+syscalls, os.Args[0], "-test.run=TestLinuxSendfileChild")
|
2012-02-14 09:34:52 +11:00
|
|
|
child.ExtraFiles = append(child.ExtraFiles, lnf)
|
|
|
|
|
child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
|
2012-02-15 11:05:51 +11:00
|
|
|
child.Stdout = &buf
|
|
|
|
|
child.Stderr = &buf
|
2013-01-24 17:32:10 +11:00
|
|
|
if err := child.Start(); err != nil {
|
|
|
|
|
t.Skipf("skipping; failed to start straced child: %v", err)
|
2012-02-14 09:34:52 +11:00
|
|
|
}
|
|
|
|
|
|
2012-02-16 09:27:26 +11:00
|
|
|
res, err := Get(fmt.Sprintf("http://%s/", ln.Addr()))
|
2012-02-14 09:34:52 +11:00
|
|
|
if err != nil {
|
2012-02-16 09:27:26 +11:00
|
|
|
t.Fatalf("http client error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
_, err = io.Copy(ioutil.Discard, res.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("client body read error: %v", err)
|
2012-02-14 09:34:52 +11:00
|
|
|
}
|
2012-02-16 09:27:26 +11:00
|
|
|
res.Body.Close()
|
2012-02-14 09:34:52 +11:00
|
|
|
|
|
|
|
|
// Force child to exit cleanly.
|
2015-01-22 15:58:25 -08:00
|
|
|
Post(fmt.Sprintf("http://%s/quit", ln.Addr()), "", nil)
|
2012-02-14 09:34:52 +11:00
|
|
|
child.Wait()
|
|
|
|
|
|
2018-01-01 17:17:14 -08:00
|
|
|
rx := regexp.MustCompile(`sendfile(64)?\(`)
|
2012-02-14 09:34:52 +11:00
|
|
|
out := buf.String()
|
2016-11-05 00:25:38 +00:00
|
|
|
if !rx.MatchString(out) {
|
2012-02-14 09:34:52 +11:00
|
|
|
t.Errorf("no sendfile system call found in:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-04 18:24:44 +00:00
|
|
|
func getBody(t *testing.T, testName string, req Request, client *Client) (*Response, []byte) {
|
|
|
|
|
r, err := client.Do(&req)
|
2010-12-10 08:51:13 +11:00
|
|
|
if err != nil {
|
2012-02-10 10:02:06 +11:00
|
|
|
t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err)
|
2010-12-10 08:51:13 +11:00
|
|
|
}
|
|
|
|
|
b, err := ioutil.ReadAll(r.Body)
|
|
|
|
|
if err != nil {
|
2012-02-10 10:02:06 +11:00
|
|
|
t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err)
|
2010-12-10 08:51:13 +11:00
|
|
|
}
|
|
|
|
|
return r, b
|
|
|
|
|
}
|
|
|
|
|
|
2012-02-14 09:34:52 +11:00
|
|
|
// TestLinuxSendfileChild isn't a real test. It's used as a helper process
|
|
|
|
|
// for TestLinuxSendfile.
|
|
|
|
|
func TestLinuxSendfileChild(*testing.T) {
|
|
|
|
|
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer os.Exit(0)
|
|
|
|
|
fd3 := os.NewFile(3, "ephemeral-port-listener")
|
|
|
|
|
ln, err := net.FileListener(fd3)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
mux := NewServeMux()
|
|
|
|
|
mux.Handle("/", FileServer(Dir("testdata")))
|
|
|
|
|
mux.HandleFunc("/quit", func(ResponseWriter, *Request) {
|
|
|
|
|
os.Exit(0)
|
|
|
|
|
})
|
|
|
|
|
s := &Server{Handler: mux}
|
|
|
|
|
err = s.Serve(ln)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
2013-07-10 13:29:52 +10:00
|
|
|
|
net/http: improve handling of errors in Dir.Open
The current implementation fails to produce an "IsNotExist" error on some
platforms (unix) for certain situations where it would be expected. This causes
downstream consumers, like FileServer, to emit 500 errors instead of a 404 for
some non-existant paths on certain platforms but not others.
As an example, os.Open("/index.html/foo") on a unix-type system will return
syscall.ENOTDIR, which os.IsNotExist cannot return true for (because the
error code is ambiguous without context). On windows, this same example
would result in os.IsNotExist returning true -- since the returned error is
specific.
This change alters Dir.Open to look up the tree for an "IsPermission" or
"IsNotExist" error to return, or a non-directory, returning os.ErrNotExist in
the last case. For all other error scenarios, the original error is returned.
This ensures that downstream code, like FileServer, receive errors that behave
the same across all platforms.
Fixes #18984
Change-Id: Id7d16591c24cd96afddb6d8ae135ac78da42ed37
Reviewed-on: https://go-review.googlesource.com/36635
Reviewed-by: Yasuhiro MATSUMOTO <mattn.jp@gmail.com>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
2017-02-08 18:42:52 -06:00
|
|
|
// Issue 18984: tests that requests for paths beyond files return not-found errors
|
|
|
|
|
func TestFileServerNotDirError(t *testing.T) {
|
|
|
|
|
defer afterTest(t)
|
|
|
|
|
ts := httptest.NewServer(FileServer(Dir("testdata")))
|
|
|
|
|
defer ts.Close()
|
|
|
|
|
|
|
|
|
|
res, err := Get(ts.URL + "/index.html/not-a-file")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
if res.StatusCode != 404 {
|
|
|
|
|
t.Errorf("StatusCode = %v; want 404", res.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-10 21:09:21 -06:00
|
|
|
test := func(name string, dir Dir) {
|
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
|
_, err = dir.Open("/index.html/not-a-file")
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("err == nil; want != nil")
|
|
|
|
|
}
|
|
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
|
t.Errorf("err = %v; os.IsNotExist(err) = %v; want true", err, os.IsNotExist(err))
|
|
|
|
|
}
|
net/http: improve handling of errors in Dir.Open
The current implementation fails to produce an "IsNotExist" error on some
platforms (unix) for certain situations where it would be expected. This causes
downstream consumers, like FileServer, to emit 500 errors instead of a 404 for
some non-existant paths on certain platforms but not others.
As an example, os.Open("/index.html/foo") on a unix-type system will return
syscall.ENOTDIR, which os.IsNotExist cannot return true for (because the
error code is ambiguous without context). On windows, this same example
would result in os.IsNotExist returning true -- since the returned error is
specific.
This change alters Dir.Open to look up the tree for an "IsPermission" or
"IsNotExist" error to return, or a non-directory, returning os.ErrNotExist in
the last case. For all other error scenarios, the original error is returned.
This ensures that downstream code, like FileServer, receive errors that behave
the same across all platforms.
Fixes #18984
Change-Id: Id7d16591c24cd96afddb6d8ae135ac78da42ed37
Reviewed-on: https://go-review.googlesource.com/36635
Reviewed-by: Yasuhiro MATSUMOTO <mattn.jp@gmail.com>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
2017-02-08 18:42:52 -06:00
|
|
|
|
2017-02-10 21:09:21 -06:00
|
|
|
_, err = dir.Open("/index.html/not-a-dir/not-a-file")
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("err == nil; want != nil")
|
|
|
|
|
}
|
|
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
|
t.Errorf("err = %v; os.IsNotExist(err) = %v; want true", err, os.IsNotExist(err))
|
|
|
|
|
}
|
|
|
|
|
})
|
net/http: improve handling of errors in Dir.Open
The current implementation fails to produce an "IsNotExist" error on some
platforms (unix) for certain situations where it would be expected. This causes
downstream consumers, like FileServer, to emit 500 errors instead of a 404 for
some non-existant paths on certain platforms but not others.
As an example, os.Open("/index.html/foo") on a unix-type system will return
syscall.ENOTDIR, which os.IsNotExist cannot return true for (because the
error code is ambiguous without context). On windows, this same example
would result in os.IsNotExist returning true -- since the returned error is
specific.
This change alters Dir.Open to look up the tree for an "IsPermission" or
"IsNotExist" error to return, or a non-directory, returning os.ErrNotExist in
the last case. For all other error scenarios, the original error is returned.
This ensures that downstream code, like FileServer, receive errors that behave
the same across all platforms.
Fixes #18984
Change-Id: Id7d16591c24cd96afddb6d8ae135ac78da42ed37
Reviewed-on: https://go-review.googlesource.com/36635
Reviewed-by: Yasuhiro MATSUMOTO <mattn.jp@gmail.com>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
2017-02-08 18:42:52 -06:00
|
|
|
}
|
2017-02-10 21:09:21 -06:00
|
|
|
|
|
|
|
|
absPath, err := filepath.Abs("testdata")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal("get abs path:", err)
|
net/http: improve handling of errors in Dir.Open
The current implementation fails to produce an "IsNotExist" error on some
platforms (unix) for certain situations where it would be expected. This causes
downstream consumers, like FileServer, to emit 500 errors instead of a 404 for
some non-existant paths on certain platforms but not others.
As an example, os.Open("/index.html/foo") on a unix-type system will return
syscall.ENOTDIR, which os.IsNotExist cannot return true for (because the
error code is ambiguous without context). On windows, this same example
would result in os.IsNotExist returning true -- since the returned error is
specific.
This change alters Dir.Open to look up the tree for an "IsPermission" or
"IsNotExist" error to return, or a non-directory, returning os.ErrNotExist in
the last case. For all other error scenarios, the original error is returned.
This ensures that downstream code, like FileServer, receive errors that behave
the same across all platforms.
Fixes #18984
Change-Id: Id7d16591c24cd96afddb6d8ae135ac78da42ed37
Reviewed-on: https://go-review.googlesource.com/36635
Reviewed-by: Yasuhiro MATSUMOTO <mattn.jp@gmail.com>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
2017-02-08 18:42:52 -06:00
|
|
|
}
|
2017-02-10 21:09:21 -06:00
|
|
|
|
|
|
|
|
test("RelativePath", Dir("testdata"))
|
|
|
|
|
test("AbsolutePath", Dir(absPath))
|
net/http: improve handling of errors in Dir.Open
The current implementation fails to produce an "IsNotExist" error on some
platforms (unix) for certain situations where it would be expected. This causes
downstream consumers, like FileServer, to emit 500 errors instead of a 404 for
some non-existant paths on certain platforms but not others.
As an example, os.Open("/index.html/foo") on a unix-type system will return
syscall.ENOTDIR, which os.IsNotExist cannot return true for (because the
error code is ambiguous without context). On windows, this same example
would result in os.IsNotExist returning true -- since the returned error is
specific.
This change alters Dir.Open to look up the tree for an "IsPermission" or
"IsNotExist" error to return, or a non-directory, returning os.ErrNotExist in
the last case. For all other error scenarios, the original error is returned.
This ensures that downstream code, like FileServer, receive errors that behave
the same across all platforms.
Fixes #18984
Change-Id: Id7d16591c24cd96afddb6d8ae135ac78da42ed37
Reviewed-on: https://go-review.googlesource.com/36635
Reviewed-by: Yasuhiro MATSUMOTO <mattn.jp@gmail.com>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
2017-02-08 18:42:52 -06:00
|
|
|
}
|
|
|
|
|
|
2014-09-15 07:14:33 -04:00
|
|
|
func TestFileServerCleanPath(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
path string
|
|
|
|
|
wantCode int
|
|
|
|
|
wantOpen []string
|
|
|
|
|
}{
|
|
|
|
|
{"/", 200, []string{"/", "/index.html"}},
|
|
|
|
|
{"/dir", 301, []string{"/dir"}},
|
|
|
|
|
{"/dir/", 200, []string{"/dir", "/dir/index.html"}},
|
|
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
var log []string
|
|
|
|
|
rr := httptest.NewRecorder()
|
|
|
|
|
req, _ := NewRequest("GET", "http://foo.localhost"+tt.path, nil)
|
|
|
|
|
FileServer(fileServerCleanPathDir{&log}).ServeHTTP(rr, req)
|
|
|
|
|
if !reflect.DeepEqual(log, tt.wantOpen) {
|
|
|
|
|
t.Logf("For %s: Opens = %q; want %q", tt.path, log, tt.wantOpen)
|
|
|
|
|
}
|
|
|
|
|
if rr.Code != tt.wantCode {
|
|
|
|
|
t.Logf("For %s: Response code = %d; want %d", tt.path, rr.Code, tt.wantCode)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type fileServerCleanPathDir struct {
|
|
|
|
|
log *[]string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (d fileServerCleanPathDir) Open(path string) (File, error) {
|
|
|
|
|
*(d.log) = append(*(d.log), path)
|
|
|
|
|
if path == "/" || path == "/dir" || path == "/dir/" {
|
|
|
|
|
// Just return back something that's a directory.
|
|
|
|
|
return Dir(".").Open(".")
|
|
|
|
|
}
|
|
|
|
|
return nil, os.ErrNotExist
|
|
|
|
|
}
|
|
|
|
|
|
2013-07-10 13:29:52 +10:00
|
|
|
type panicOnSeek struct{ io.ReadSeeker }
|
2016-10-25 12:51:39 -07:00
|
|
|
|
|
|
|
|
func Test_scanETag(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
in string
|
|
|
|
|
wantETag string
|
|
|
|
|
wantRemain string
|
|
|
|
|
}{
|
|
|
|
|
{`W/"etag-1"`, `W/"etag-1"`, ""},
|
|
|
|
|
{`"etag-2"`, `"etag-2"`, ""},
|
|
|
|
|
{`"etag-1", "etag-2"`, `"etag-1"`, `, "etag-2"`},
|
|
|
|
|
{"", "", ""},
|
|
|
|
|
{"W/", "", ""},
|
|
|
|
|
{`W/"truc`, "", ""},
|
|
|
|
|
{`w/"case-sensitive"`, "", ""},
|
2017-04-06 12:24:58 +01:00
|
|
|
{`"spaced etag"`, "", ""},
|
2016-10-25 12:51:39 -07:00
|
|
|
}
|
|
|
|
|
for _, test := range tests {
|
|
|
|
|
etag, remain := ExportScanETag(test.in)
|
|
|
|
|
if etag != test.wantETag || remain != test.wantRemain {
|
|
|
|
|
t.Errorf("scanETag(%q)=%q %q, want %q %q", test.in, etag, remain, test.wantETag, test.wantRemain)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|