Commit f3862742 authored by Dan Harrington's avatar Dan Harrington Committed by Brad Fitzpatrick

net/http: support If-Match in ServeContent

- Added support for If-Match and If-Unmodified-Since.
- Precondition checks now more strictly follow RFC 7232 section 6, which
affects precedence when multiple condition headers are present.
- When serving a 304, Last-Modified header is now removed when no ETag is
present (as suggested by RFC 7232 section 4.1).
- If-None-Match supports multiple ETags.
- ETag comparison now correctly handles weak ETags.

Fixes #17572

Change-Id: I35039dea6811480ccf2889f8ed9c6a39ce34bfff
Reviewed-on: https://go-review.googlesource.com/32014Reviewed-by: 's avatarBrad Fitzpatrick <bradfitz@golang.org>
parent 18f0e881
...@@ -24,6 +24,7 @@ var ( ...@@ -24,6 +24,7 @@ var (
ExportErrRequestCanceled = errRequestCanceled ExportErrRequestCanceled = errRequestCanceled
ExportErrRequestCanceledConn = errRequestCanceledConn ExportErrRequestCanceledConn = errRequestCanceledConn
ExportServeFile = serveFile ExportServeFile = serveFile
ExportScanETag = scanETag
ExportHttp2ConfigureServer = http2ConfigureServer ExportHttp2ConfigureServer = http2ConfigureServer
) )
......
This diff is collapsed.
...@@ -784,8 +784,9 @@ func TestServeContent(t *testing.T) { ...@@ -784,8 +784,9 @@ func TestServeContent(t *testing.T) {
wantStatus: 200, wantStatus: 200,
}, },
"not_modified_modtime": { "not_modified_modtime": {
file: "testdata/style.css", file: "testdata/style.css",
modtime: htmlModTime, serveETag: `"foo"`, // Last-Modified sent only when no ETag
modtime: htmlModTime,
reqHeader: map[string]string{ reqHeader: map[string]string{
"If-Modified-Since": htmlModTime.UTC().Format(TimeFormat), "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
}, },
...@@ -794,6 +795,7 @@ func TestServeContent(t *testing.T) { ...@@ -794,6 +795,7 @@ func TestServeContent(t *testing.T) {
"not_modified_modtime_with_contenttype": { "not_modified_modtime_with_contenttype": {
file: "testdata/style.css", file: "testdata/style.css",
serveContentType: "text/css", // explicit content type serveContentType: "text/css", // explicit content type
serveETag: `"foo"`, // Last-Modified sent only when no ETag
modtime: htmlModTime, modtime: htmlModTime,
reqHeader: map[string]string{ reqHeader: map[string]string{
"If-Modified-Since": htmlModTime.UTC().Format(TimeFormat), "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
...@@ -810,12 +812,21 @@ func TestServeContent(t *testing.T) { ...@@ -810,12 +812,21 @@ func TestServeContent(t *testing.T) {
}, },
"not_modified_etag_no_seek": { "not_modified_etag_no_seek": {
content: panicOnSeek{nil}, // should never be called content: panicOnSeek{nil}, // should never be called
serveETag: `"foo"`, serveETag: `W/"foo"`, // If-None-Match uses weak ETag comparison
reqHeader: map[string]string{ reqHeader: map[string]string{
"If-None-Match": `"foo"`, "If-None-Match": `"baz", W/"foo"`,
}, },
wantStatus: 304, wantStatus: 304,
}, },
"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",
},
"range_good": { "range_good": {
file: "testdata/style.css", file: "testdata/style.css",
serveETag: `"A"`, serveETag: `"A"`,
...@@ -826,6 +837,27 @@ func TestServeContent(t *testing.T) { ...@@ -826,6 +837,27 @@ func TestServeContent(t *testing.T) {
wantContentType: "text/css; charset=utf-8", wantContentType: "text/css; charset=utf-8",
wantContentRange: "bytes 0-4/8", wantContentRange: "bytes 0-4/8",
}, },
"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",
},
"range_no_overlap": { "range_no_overlap": {
file: "testdata/style.css", file: "testdata/style.css",
serveETag: `"A"`, serveETag: `"A"`,
...@@ -878,6 +910,62 @@ func TestServeContent(t *testing.T) { ...@@ -878,6 +910,62 @@ func TestServeContent(t *testing.T) {
wantStatus: StatusOK, wantStatus: StatusOK,
wantContentType: "text/html; charset=utf-8", wantContentType: "text/html; charset=utf-8",
}, },
"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"`,
},
wantStatus: 412,
wantContentType: "text/plain; charset=utf-8",
},
"ifmatch_fails_on_weak_etag": {
file: "testdata/style.css",
serveETag: `W/"A"`,
reqHeader: map[string]string{
"If-Match": `W/"A"`,
},
wantStatus: 412,
wantContentType: "text/plain; charset=utf-8",
},
"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),
},
wantStatus: 412,
wantContentType: "text/plain; charset=utf-8",
wantLastMod: htmlModTime.UTC().Format(TimeFormat),
},
} }
for testName, tt := range tests { for testName, tt := range tests {
var content io.ReadSeeker var content io.ReadSeeker
...@@ -1108,3 +1196,26 @@ func (d fileServerCleanPathDir) Open(path string) (File, error) { ...@@ -1108,3 +1196,26 @@ func (d fileServerCleanPathDir) Open(path string) (File, error) {
} }
type panicOnSeek struct{ io.ReadSeeker } type panicOnSeek struct{ io.ReadSeeker }
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"`, "", ""},
}
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)
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment