mirror of
https://github.com/minio/minio.git
synced 2024-12-26 07:05:55 -05:00
7e76d66184
In a reverse proxying setup, a proxy in front of MinIO may attempt to request objects in slices for enhanced cache efficiency. Since such a a proxy cannot have prior knowledge of how large a requested resource is, it usually sends a header of the form: Range: 0-$slice_size ... and, depending on the size of the resource, expects either: - an empty response, if $resource_size == 0 - a full response, if $resource_size <= $slice_size - a partial response, if $resource_size > $slice_size Prior to this change, MinIO would respond 416 Range Not Satisfiable if a client tried to request a range on an empty resource. This behavior is technically consistent with RFC9110[1] – However, it renders sliced reverse proxying, such as implemented in Nginx, broken in the case of empty files. Nginx itself seems to break this convention to enable "useful" responses in these cases, and MinIO should probably do that too. [1]: https://www.rfc-editor.org/rfc/rfc9110#byte.ranges
203 lines
5.9 KiB
Go
203 lines
5.9 KiB
Go
// Copyright (c) 2015-2021 MinIO, Inc.
|
|
//
|
|
// This file is part of MinIO Object Storage stack
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
byteRangePrefix = "bytes="
|
|
)
|
|
|
|
// HTTPRangeSpec represents a range specification as supported by S3 GET
|
|
// object request.
|
|
//
|
|
// Case 1: Not present -> represented by a nil RangeSpec
|
|
// Case 2: bytes=1-10 (absolute start and end offsets) -> RangeSpec{false, 1, 10}
|
|
// Case 3: bytes=10- (absolute start offset with end offset unspecified) -> RangeSpec{false, 10, -1}
|
|
// Case 4: bytes=-30 (suffix length specification) -> RangeSpec{true, -30, -1}
|
|
type HTTPRangeSpec struct {
|
|
// Does the range spec refer to a suffix of the object?
|
|
IsSuffixLength bool
|
|
|
|
// Start and end offset specified in range spec
|
|
Start, End int64
|
|
}
|
|
|
|
// GetLength - get length of range
|
|
func (h *HTTPRangeSpec) GetLength(resourceSize int64) (rangeLength int64, err error) {
|
|
switch {
|
|
case resourceSize < 0:
|
|
return 0, errors.New("Resource size cannot be negative")
|
|
|
|
case h == nil:
|
|
rangeLength = resourceSize
|
|
|
|
case h.IsSuffixLength:
|
|
specifiedLen := -h.Start
|
|
rangeLength = specifiedLen
|
|
if specifiedLen > resourceSize {
|
|
rangeLength = resourceSize
|
|
}
|
|
|
|
case h.Start == 0 && resourceSize == 0:
|
|
rangeLength = resourceSize
|
|
|
|
case h.Start >= resourceSize:
|
|
return 0, errInvalidRange
|
|
|
|
case h.End > -1:
|
|
end := h.End
|
|
if resourceSize <= end {
|
|
end = resourceSize - 1
|
|
}
|
|
rangeLength = end - h.Start + 1
|
|
|
|
case h.End == -1:
|
|
rangeLength = resourceSize - h.Start
|
|
|
|
default:
|
|
return 0, errors.New("Unexpected range specification case")
|
|
}
|
|
|
|
return rangeLength, nil
|
|
}
|
|
|
|
// GetOffsetLength computes the start offset and length of the range
|
|
// given the size of the resource
|
|
func (h *HTTPRangeSpec) GetOffsetLength(resourceSize int64) (start, length int64, err error) {
|
|
if h == nil {
|
|
// No range specified, implies whole object.
|
|
return 0, resourceSize, nil
|
|
}
|
|
|
|
length, err = h.GetLength(resourceSize)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
start = h.Start
|
|
if h.IsSuffixLength {
|
|
start = resourceSize + h.Start
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
}
|
|
return start, length, nil
|
|
}
|
|
|
|
// Parse a HTTP range header value into a HTTPRangeSpec
|
|
func parseRequestRangeSpec(rangeString string) (hrange *HTTPRangeSpec, err error) {
|
|
// Return error if given range string doesn't start with byte range prefix.
|
|
if !strings.HasPrefix(rangeString, byteRangePrefix) {
|
|
return nil, fmt.Errorf("'%s' does not start with '%s'", rangeString, byteRangePrefix)
|
|
}
|
|
|
|
// Trim byte range prefix.
|
|
byteRangeString := strings.TrimPrefix(rangeString, byteRangePrefix)
|
|
|
|
// Check if range string contains delimiter '-', else return error. eg. "bytes=8"
|
|
sepIndex := strings.Index(byteRangeString, "-")
|
|
if sepIndex == -1 {
|
|
return nil, fmt.Errorf("'%s' does not have a valid range value", rangeString)
|
|
}
|
|
|
|
offsetBeginString := byteRangeString[:sepIndex]
|
|
offsetBegin := int64(-1)
|
|
// Convert offsetBeginString only if its not empty.
|
|
if len(offsetBeginString) > 0 {
|
|
if offsetBeginString[0] == '+' {
|
|
return nil, fmt.Errorf("Byte position ('%s') must not have a sign", offsetBeginString)
|
|
} else if offsetBegin, err = strconv.ParseInt(offsetBeginString, 10, 64); err != nil {
|
|
return nil, fmt.Errorf("'%s' does not have a valid first byte position value", rangeString)
|
|
} else if offsetBegin < 0 {
|
|
return nil, fmt.Errorf("First byte position is negative ('%d')", offsetBegin)
|
|
}
|
|
}
|
|
|
|
offsetEndString := byteRangeString[sepIndex+1:]
|
|
offsetEnd := int64(-1)
|
|
// Convert offsetEndString only if its not empty.
|
|
if len(offsetEndString) > 0 {
|
|
if offsetEndString[0] == '+' {
|
|
return nil, fmt.Errorf("Byte position ('%s') must not have a sign", offsetEndString)
|
|
} else if offsetEnd, err = strconv.ParseInt(offsetEndString, 10, 64); err != nil {
|
|
return nil, fmt.Errorf("'%s' does not have a valid last byte position value", rangeString)
|
|
} else if offsetEnd < 0 {
|
|
return nil, fmt.Errorf("Last byte position is negative ('%d')", offsetEnd)
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case offsetBegin > -1 && offsetEnd > -1:
|
|
if offsetBegin > offsetEnd {
|
|
return nil, errInvalidRange
|
|
}
|
|
return &HTTPRangeSpec{false, offsetBegin, offsetEnd}, nil
|
|
case offsetBegin > -1:
|
|
return &HTTPRangeSpec{false, offsetBegin, -1}, nil
|
|
case offsetEnd > -1:
|
|
if offsetEnd == 0 {
|
|
return nil, errInvalidRange
|
|
}
|
|
return &HTTPRangeSpec{true, -offsetEnd, -1}, nil
|
|
default:
|
|
// rangeString contains first and last byte positions missing. eg. "bytes=-"
|
|
return nil, fmt.Errorf("'%s' does not have valid range value", rangeString)
|
|
}
|
|
}
|
|
|
|
// String returns stringified representation of range for a particular resource size.
|
|
func (h *HTTPRangeSpec) String(resourceSize int64) string {
|
|
if h == nil {
|
|
return ""
|
|
}
|
|
off, length, err := h.GetOffsetLength(resourceSize)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%d-%d", off, off+length-1)
|
|
}
|
|
|
|
// ToHeader returns the Range header value.
|
|
func (h *HTTPRangeSpec) ToHeader() (string, error) {
|
|
if h == nil {
|
|
return "", nil
|
|
}
|
|
start := strconv.Itoa(int(h.Start))
|
|
end := strconv.Itoa(int(h.End))
|
|
switch {
|
|
case h.Start >= 0 && h.End >= 0:
|
|
if h.Start > h.End {
|
|
return "", errInvalidRange
|
|
}
|
|
case h.IsSuffixLength:
|
|
end = strconv.Itoa(int(h.Start * -1))
|
|
start = ""
|
|
case h.Start > -1:
|
|
end = ""
|
|
default:
|
|
return "", fmt.Errorf("does not have valid range value")
|
|
}
|
|
return fmt.Sprintf("bytes=%s-%s", start, end), nil
|
|
}
|