Commit aa9b3d70 authored by Sina Siadat's avatar Sina Siadat Committed by Brad Fitzpatrick

net/http: send Content-Range if no byte range overlaps

RFC 7233, section 4.4 says:
>>>
For byte ranges, failing to overlap the current extent means that the
first-byte-pos of all of the byte-range-spec values were greater than the
current length of the selected representation.  When this status code is
generated in response to a byte-range request, the sender SHOULD generate a
Content-Range header field specifying the current length of the selected
representation
<<<

Thus, we should send the Content-Range only if none of the ranges
overlap.

Fixes #15798.

Change-Id: Ic9a3e1b3a8730398b4bdff877a8f2fd2e30149e3
Reviewed-on: https://go-review.googlesource.com/24212
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: default avatarBrad Fitzpatrick <bradfitz@golang.org>
parent 0bc94a88
...@@ -140,6 +140,10 @@ func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time ...@@ -140,6 +140,10 @@ func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time
// users. // users.
var errSeeker = errors.New("seeker can't seek") var errSeeker = errors.New("seeker can't seek")
// errNoOverlap is returned by serveContent's parseRange if first-byte-pos of
// all of the byte-range-spec values is greater than the content size.
var errNoOverlap = errors.New("invalid range: failed to overlap")
// if name is empty, filename is unknown. (used for mime type, before sniffing) // if name is empty, filename is unknown. (used for mime type, before sniffing)
// if modtime.IsZero(), modtime is unknown. // if modtime.IsZero(), modtime is unknown.
// content must be seeked to the beginning of the file. // content must be seeked to the beginning of the file.
...@@ -189,6 +193,9 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, ...@@ -189,6 +193,9 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
if size >= 0 { if size >= 0 {
ranges, err := parseRange(rangeReq, size) ranges, err := parseRange(rangeReq, size)
if err != nil { if err != nil {
if err == errNoOverlap {
w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", size))
}
Error(w, err.Error(), StatusRequestedRangeNotSatisfiable) Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
return return
} }
...@@ -543,6 +550,7 @@ func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHead ...@@ -543,6 +550,7 @@ func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHead
} }
// parseRange parses a Range header string as per RFC 2616. // parseRange parses a Range header string as per RFC 2616.
// errNoOverlap is returned if none of the ranges overlap.
func parseRange(s string, size int64) ([]httpRange, error) { func parseRange(s string, size int64) ([]httpRange, error) {
if s == "" { if s == "" {
return nil, nil // header not present return nil, nil // header not present
...@@ -552,6 +560,7 @@ func parseRange(s string, size int64) ([]httpRange, error) { ...@@ -552,6 +560,7 @@ func parseRange(s string, size int64) ([]httpRange, error) {
return nil, errors.New("invalid range") return nil, errors.New("invalid range")
} }
var ranges []httpRange var ranges []httpRange
noOverlap := false
for _, ra := range strings.Split(s[len(b):], ",") { for _, ra := range strings.Split(s[len(b):], ",") {
ra = strings.TrimSpace(ra) ra = strings.TrimSpace(ra)
if ra == "" { if ra == "" {
...@@ -577,9 +586,15 @@ func parseRange(s string, size int64) ([]httpRange, error) { ...@@ -577,9 +586,15 @@ func parseRange(s string, size int64) ([]httpRange, error) {
r.length = size - r.start r.length = size - r.start
} else { } else {
i, err := strconv.ParseInt(start, 10, 64) i, err := strconv.ParseInt(start, 10, 64)
if err != nil || i >= size || i < 0 { if err != nil || i < 0 {
return nil, errors.New("invalid range") return nil, errors.New("invalid range")
} }
if i >= size {
// If the range begins after the size of the content,
// then it does not overlap.
noOverlap = true
continue
}
r.start = i r.start = i
if end == "" { if end == "" {
// If no end is specified, range extends to end of the file. // If no end is specified, range extends to end of the file.
...@@ -597,6 +612,10 @@ func parseRange(s string, size int64) ([]httpRange, error) { ...@@ -597,6 +612,10 @@ func parseRange(s string, size int64) ([]httpRange, error) {
} }
ranges = append(ranges, r) ranges = append(ranges, r)
} }
if noOverlap && len(ranges) == 0 {
// The specified ranges did not overlap with the content.
return nil, errNoOverlap
}
return ranges, nil return ranges, nil
} }
......
...@@ -765,6 +765,7 @@ func TestServeContent(t *testing.T) { ...@@ -765,6 +765,7 @@ func TestServeContent(t *testing.T) {
reqHeader map[string]string reqHeader map[string]string
wantLastMod string wantLastMod string
wantContentType string wantContentType string
wantContentRange string
wantStatus int wantStatus int
} }
htmlModTime := mustStat(t, "testdata/index.html").ModTime() htmlModTime := mustStat(t, "testdata/index.html").ModTime()
...@@ -820,8 +821,19 @@ func TestServeContent(t *testing.T) { ...@@ -820,8 +821,19 @@ func TestServeContent(t *testing.T) {
reqHeader: map[string]string{ reqHeader: map[string]string{
"Range": "bytes=0-4", "Range": "bytes=0-4",
}, },
wantStatus: StatusPartialContent, wantStatus: StatusPartialContent,
wantContentType: "text/css; charset=utf-8", wantContentType: "text/css; charset=utf-8",
wantContentRange: "bytes 0-4/8",
},
"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",
}, },
// An If-Range resource for entity "A", but entity "B" is now current. // An If-Range resource for entity "A", but entity "B" is now current.
// The Range request should be ignored. // The Range request should be ignored.
...@@ -842,9 +854,10 @@ func TestServeContent(t *testing.T) { ...@@ -842,9 +854,10 @@ func TestServeContent(t *testing.T) {
"Range": "bytes=0-4", "Range": "bytes=0-4",
"If-Range": "Wed, 25 Jun 2014 17:12:18 GMT", "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
}, },
wantStatus: StatusPartialContent, wantStatus: StatusPartialContent,
wantContentType: "text/css; charset=utf-8", wantContentType: "text/css; charset=utf-8",
wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT", wantContentRange: "bytes 0-4/8",
wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
}, },
"range_with_modtime_nanos": { "range_with_modtime_nanos": {
file: "testdata/style.css", file: "testdata/style.css",
...@@ -853,9 +866,10 @@ func TestServeContent(t *testing.T) { ...@@ -853,9 +866,10 @@ func TestServeContent(t *testing.T) {
"Range": "bytes=0-4", "Range": "bytes=0-4",
"If-Range": "Wed, 25 Jun 2014 17:12:18 GMT", "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
}, },
wantStatus: StatusPartialContent, wantStatus: StatusPartialContent,
wantContentType: "text/css; charset=utf-8", wantContentType: "text/css; charset=utf-8",
wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT", wantContentRange: "bytes 0-4/8",
wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
}, },
"unix_zero_modtime": { "unix_zero_modtime": {
content: strings.NewReader("<html>foo"), content: strings.NewReader("<html>foo"),
...@@ -903,6 +917,9 @@ func TestServeContent(t *testing.T) { ...@@ -903,6 +917,9 @@ func TestServeContent(t *testing.T) {
if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e { if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e {
t.Errorf("test %q: content-type = %q, want %q", testName, g, e) t.Errorf("test %q: content-type = %q, want %q", testName, g, e)
} }
if g, e := res.Header.Get("Content-Range"), tt.wantContentRange; g != e {
t.Errorf("test %q: content-range = %q, want %q", testName, g, e)
}
if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e { if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e {
t.Errorf("test %q: last-modified = %q, want %q", testName, g, e) t.Errorf("test %q: last-modified = %q, want %q", testName, g, e)
} }
......
...@@ -38,7 +38,7 @@ var ParseRangeTests = []struct { ...@@ -38,7 +38,7 @@ var ParseRangeTests = []struct {
{"bytes=0-", 10, []httpRange{{0, 10}}}, {"bytes=0-", 10, []httpRange{{0, 10}}},
{"bytes=5-", 10, []httpRange{{5, 5}}}, {"bytes=5-", 10, []httpRange{{5, 5}}},
{"bytes=0-20", 10, []httpRange{{0, 10}}}, {"bytes=0-20", 10, []httpRange{{0, 10}}},
{"bytes=15-,0-5", 10, nil}, {"bytes=15-,0-5", 10, []httpRange{{0, 6}}},
{"bytes=1-2,5-", 10, []httpRange{{1, 2}, {5, 5}}}, {"bytes=1-2,5-", 10, []httpRange{{1, 2}, {5, 5}}},
{"bytes=-2 , 7-", 11, []httpRange{{9, 2}, {7, 4}}}, {"bytes=-2 , 7-", 11, []httpRange{{9, 2}, {7, 4}}},
{"bytes=0-0 ,2-2, 7-", 11, []httpRange{{0, 1}, {2, 1}, {7, 4}}}, {"bytes=0-0 ,2-2, 7-", 11, []httpRange{{0, 1}, {2, 1}, {7, 4}}},
......
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