Files
rclone/lib/rest/headers.go
T

191 lines
5.4 KiB
Go

package rest
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/rclone/rclone/fs"
)
// ContentRange is a parsed Content-Range response header.
type ContentRange struct {
Start int64 // first byte position of the range
End int64 // last byte position of the range (inclusive)
Size int64 // complete length of the representation, or -1 if unknown
}
// ParseContentRange parses a Content-Range header of the form
// "bytes start-end/size" as returned with a ranged (206) response.
// The Size is -1 if the complete length is unknown ("*").
func ParseContentRange(value string) (ContentRange, error) {
const prefix = "bytes "
if !strings.HasPrefix(value, prefix) {
return ContentRange{}, fmt.Errorf("doesn't start with %q", prefix)
}
rangeAndSize := strings.Split(value[len(prefix):], "/")
if len(rangeAndSize) != 2 {
return ContentRange{}, errors.New("must contain one '/'")
}
bounds := strings.Split(rangeAndSize[0], "-")
if len(bounds) != 2 {
return ContentRange{}, errors.New("must contain one '-'")
}
start, err := strconv.ParseInt(bounds[0], 10, 64)
if err != nil || start < 0 {
return ContentRange{}, errors.New("invalid start")
}
end, err := strconv.ParseInt(bounds[1], 10, 64)
if err != nil || end < start {
return ContentRange{}, errors.New("invalid end")
}
size := int64(-1)
if rangeAndSize[1] != "*" {
size, err = strconv.ParseInt(rangeAndSize[1], 10, 64)
if err != nil || size < 0 || end >= size {
return ContentRange{}, errors.New("invalid complete length")
}
}
return ContentRange{Start: start, End: end, Size: size}, nil
}
// CheckContentRange checks that a response satisfies a Range open option.
// The size is the expected size of the complete representation, or -1 if it is
// unknown. Calls without a Range option are ignored.
func CheckContentRange(resp *http.Response, options []fs.OpenOption, size int64) error {
var requestRange string
for _, option := range options {
key, value := option.Header()
if strings.EqualFold(key, "Range") {
requestRange = value
}
}
if requestRange == "" {
return nil
}
requested, err := fs.ParseRangeOption(requestRange)
if err != nil {
return fmt.Errorf("invalid requested range %q: %w", requestRange, err)
}
if requested.Start < 0 && requested.End < 0 {
return fmt.Errorf("invalid requested range %q", requestRange)
}
if requested.Start >= 0 && requested.End >= 0 && requested.End < requested.Start {
return fmt.Errorf("invalid requested range %q", requestRange)
}
if resp == nil {
return errors.New("nil response to range request")
}
if resp.StatusCode == http.StatusOK {
responseSize := size
if resp.ContentLength >= 0 {
if size >= 0 && resp.ContentLength != size {
return fmt.Errorf("Content-Length %d does not match expected size %d", resp.ContentLength, size)
}
responseSize = resp.ContentLength
}
if responseSize >= 0 {
offset, limit := requested.Decode(responseSize)
if limit < 0 {
limit = responseSize - offset
}
if offset == 0 && limit >= responseSize {
return nil
}
} else if requested.Start == 0 && requested.End < 0 {
return nil
}
return fmt.Errorf("%w %q", fs.ErrorRangeIgnored, requestRange)
}
if resp.StatusCode != http.StatusPartialContent {
return fmt.Errorf("response status %d does not satisfy requested range %q", resp.StatusCode, requestRange)
}
responseRange := resp.Header.Get("Content-Range")
got, err := ParseContentRange(responseRange)
if err != nil {
return fmt.Errorf("invalid Content-Range %q: %w", responseRange, err)
}
if size >= 0 && got.Size >= 0 && got.Size != size {
return fmt.Errorf("Content-Range %q does not match expected size %d", responseRange, size)
}
rangeSize := size
if rangeSize < 0 {
rangeSize = got.Size
}
expectedStart := requested.Start
expectedEnd := requested.End
if requested.Start >= 0 {
if rangeSize >= 0 && (expectedEnd < 0 || expectedEnd >= rangeSize) {
expectedEnd = rangeSize - 1
}
} else if rangeSize >= 0 {
expectedStart = max(rangeSize-requested.End, 0)
expectedEnd = rangeSize - 1
}
var matches bool
if requested.Start < 0 && rangeSize < 0 {
matches = got.End-got.Start+1 <= requested.End
} else {
matches = got.Start == expectedStart
if expectedEnd >= 0 {
matches = matches && got.End == expectedEnd
}
}
if !matches {
return fmt.Errorf("Content-Range %q does not match requested range %q", responseRange, requestRange)
}
contentLength := got.End - got.Start + 1
if contentLength <= 0 {
return fmt.Errorf("invalid Content-Range %q: length overflows", responseRange)
}
if resp.ContentLength >= 0 && resp.ContentLength != contentLength {
return fmt.Errorf("Content-Length %d does not match Content-Range %q", resp.ContentLength, responseRange)
}
return nil
}
// ParseSizeFromHeaders parses HTTP response headers to get the full file size.
// Returns -1 if the headers did not exist or were invalid.
func ParseSizeFromHeaders(headers http.Header) (size int64) {
size = -1
var contentLength = headers.Get("Content-Length")
if len(contentLength) != 0 {
var err error
if size, err = strconv.ParseInt(contentLength, 10, 64); err != nil {
return -1
}
}
var contentRange = headers.Get("Content-Range")
if len(contentRange) == 0 {
return size
}
if !strings.HasPrefix(contentRange, "bytes ") {
return -1
}
slash := strings.IndexRune(contentRange, '/')
if slash < 0 {
return -1
}
ret, err := strconv.ParseInt(contentRange[slash+1:], 10, 64)
if err != nil {
return -1
}
return ret
}