mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	This adds support for parsing Range and Content-Range headers according to RFC 7230. The package could be extended in the future to handle other headers. Updates tailscale/corp#14772 Signed-off-by: Joe Tsai <joetsai@digital-static.net>
		
			
				
	
	
		
			198 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			198 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| // Package httphdr implements functionality for parsing and formatting
 | |
| // standard HTTP headers.
 | |
| package httphdr
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| )
 | |
| 
 | |
| // Range is a range of bytes within some content.
 | |
| type Range struct {
 | |
| 	// Start is the starting offset.
 | |
| 	// It is zero if Length is negative; it must not be negative.
 | |
| 	Start int64
 | |
| 	// Length is the length of the content.
 | |
| 	// It is zero if the length extends to the end of the content.
 | |
| 	// It is negative if the length is relative to the end (e.g., last 5 bytes).
 | |
| 	Length int64
 | |
| }
 | |
| 
 | |
| // ows is optional whitespace.
 | |
| const ows = " \t" // per RFC 7230, section 3.2.3
 | |
| 
 | |
| // ParseRange parses a "Range" header per RFC 7233, section 3.
 | |
| // It only handles "Range" headers where the units is "bytes".
 | |
| // The "Range" header is usually only specified in GET requests.
 | |
| func ParseRange(hdr string) (ranges []Range, ok bool) {
 | |
| 	// Grammar per RFC 7233, appendix D:
 | |
| 	//	Range = byte-ranges-specifier | other-ranges-specifier
 | |
| 	//	byte-ranges-specifier = bytes-unit "=" byte-range-set
 | |
| 	//	bytes-unit = "bytes"
 | |
| 	//	byte-range-set =
 | |
| 	//		*("," OWS)
 | |
| 	//		(byte-range-spec | suffix-byte-range-spec)
 | |
| 	//		*(OWS "," [OWS ( byte-range-spec | suffix-byte-range-spec )])
 | |
| 	//	byte-range-spec = first-byte-pos "-" [last-byte-pos]
 | |
| 	//	suffix-byte-range-spec = "-" suffix-length
 | |
| 	// We do not support other-ranges-specifier.
 | |
| 	// All other identifiers are 1*DIGIT.
 | |
| 	hdr = strings.Trim(hdr, ows) // per RFC 7230, section 3.2
 | |
| 	units, elems, hasUnits := strings.Cut(hdr, "=")
 | |
| 	elems = strings.TrimLeft(elems, ","+ows)
 | |
| 	for _, elem := range strings.Split(elems, ",") {
 | |
| 		elem = strings.Trim(elem, ows) // per RFC 7230, section 7
 | |
| 		switch {
 | |
| 		case strings.HasPrefix(elem, "-"): // i.e., "-" suffix-length
 | |
| 			n, ok := parseNumber(strings.TrimPrefix(elem, "-"))
 | |
| 			if !ok {
 | |
| 				return ranges, false
 | |
| 			}
 | |
| 			ranges = append(ranges, Range{0, -n})
 | |
| 		case strings.HasSuffix(elem, "-"): // i.e., first-byte-pos "-"
 | |
| 			n, ok := parseNumber(strings.TrimSuffix(elem, "-"))
 | |
| 			if !ok {
 | |
| 				return ranges, false
 | |
| 			}
 | |
| 			ranges = append(ranges, Range{n, 0})
 | |
| 		default: // i.e., first-byte-pos "-" last-byte-pos
 | |
| 			prefix, suffix, hasDash := strings.Cut(elem, "-")
 | |
| 			n, ok2 := parseNumber(prefix)
 | |
| 			m, ok3 := parseNumber(suffix)
 | |
| 			if !hasDash || !ok2 || !ok3 || m < n {
 | |
| 				return ranges, false
 | |
| 			}
 | |
| 			ranges = append(ranges, Range{n, m - n + 1})
 | |
| 		}
 | |
| 	}
 | |
| 	return ranges, units == "bytes" && hasUnits && len(ranges) > 0 // must see at least one element per RFC 7233, section 2.1
 | |
| }
 | |
| 
 | |
| // FormatRange formats a "Range" header per RFC 7233, section 3.
 | |
| // It only handles "Range" headers where the units is "bytes".
 | |
| // The "Range" header is usually only specified in GET requests.
 | |
| func FormatRange(ranges []Range) (hdr string, ok bool) {
 | |
| 	b := []byte("bytes=")
 | |
| 	for _, r := range ranges {
 | |
| 		switch {
 | |
| 		case r.Length > 0: // i.e., first-byte-pos "-" last-byte-pos
 | |
| 			if r.Start < 0 {
 | |
| 				return string(b), false
 | |
| 			}
 | |
| 			b = strconv.AppendUint(b, uint64(r.Start), 10)
 | |
| 			b = append(b, '-')
 | |
| 			b = strconv.AppendUint(b, uint64(r.Start+r.Length-1), 10)
 | |
| 			b = append(b, ',')
 | |
| 		case r.Length == 0: // i.e., first-byte-pos "-"
 | |
| 			if r.Start < 0 {
 | |
| 				return string(b), false
 | |
| 			}
 | |
| 			b = strconv.AppendUint(b, uint64(r.Start), 10)
 | |
| 			b = append(b, '-')
 | |
| 			b = append(b, ',')
 | |
| 		case r.Length < 0: // i.e., "-" suffix-length
 | |
| 			if r.Start != 0 {
 | |
| 				return string(b), false
 | |
| 			}
 | |
| 			b = append(b, '-')
 | |
| 			b = strconv.AppendUint(b, uint64(-r.Length), 10)
 | |
| 			b = append(b, ',')
 | |
| 		default:
 | |
| 			return string(b), false
 | |
| 		}
 | |
| 	}
 | |
| 	return string(bytes.TrimRight(b, ",")), len(ranges) > 0
 | |
| }
 | |
| 
 | |
| // ParseContentRange parses a "Content-Range" header per RFC 7233, section 4.2.
 | |
| // It only handles "Content-Range" headers where the units is "bytes".
 | |
| // The "Content-Range" header is usually only specified in HTTP responses.
 | |
| //
 | |
| // If only the completeLength is specified, then start and length are both zero.
 | |
| //
 | |
| // Otherwise, the parses the start and length and the optional completeLength,
 | |
| // which is -1 if unspecified. The start is non-negative and the length is positive.
 | |
| func ParseContentRange(hdr string) (start, length, completeLength int64, ok bool) {
 | |
| 	// Grammar per RFC 7233, appendix D:
 | |
| 	//	Content-Range = byte-content-range | other-content-range
 | |
| 	//	byte-content-range = bytes-unit SP (byte-range-resp | unsatisfied-range)
 | |
| 	//	bytes-unit = "bytes"
 | |
| 	//	byte-range-resp = byte-range "/" (complete-length | "*")
 | |
| 	//	unsatisfied-range = "*/" complete-length
 | |
| 	//	byte-range = first-byte-pos "-" last-byte-pos
 | |
| 	// We do not support other-content-range.
 | |
| 	// All other identifiers are 1*DIGIT.
 | |
| 	hdr = strings.Trim(hdr, ows) // per RFC 7230, section 3.2
 | |
| 	suffix, hasUnits := strings.CutPrefix(hdr, "bytes ")
 | |
| 	suffix, unsatisfied := strings.CutPrefix(suffix, "*/")
 | |
| 	if unsatisfied { // i.e., unsatisfied-range
 | |
| 		n, ok := parseNumber(suffix)
 | |
| 		if !ok {
 | |
| 			return start, length, completeLength, false
 | |
| 		}
 | |
| 		completeLength = n
 | |
| 	} else { // i.e., byte-range "/" (complete-length | "*")
 | |
| 		prefix, suffix, hasDash := strings.Cut(suffix, "-")
 | |
| 		middle, suffix, hasSlash := strings.Cut(suffix, "/")
 | |
| 		n, ok0 := parseNumber(prefix)
 | |
| 		m, ok1 := parseNumber(middle)
 | |
| 		o, ok2 := parseNumber(suffix)
 | |
| 		if suffix == "*" {
 | |
| 			o, ok2 = -1, true
 | |
| 		}
 | |
| 		if !hasDash || !hasSlash || !ok0 || !ok1 || !ok2 || m < n || (o >= 0 && o <= m) {
 | |
| 			return start, length, completeLength, false
 | |
| 		}
 | |
| 		start = n
 | |
| 		length = m - n + 1
 | |
| 		completeLength = o
 | |
| 	}
 | |
| 	return start, length, completeLength, hasUnits
 | |
| }
 | |
| 
 | |
| // FormatContentRange parses a "Content-Range" header per RFC 7233, section 4.2.
 | |
| // It only handles "Content-Range" headers where the units is "bytes".
 | |
| // The "Content-Range" header is usually only specified in HTTP responses.
 | |
| //
 | |
| // If start and length are non-positive, then it encodes just the completeLength,
 | |
| // which must be a non-negative value.
 | |
| //
 | |
| // Otherwise, it encodes the start and length as a byte-range,
 | |
| // and optionally emits the complete length if it is non-negative.
 | |
| // The length must be positive (as RFC 7233 uses inclusive end offsets).
 | |
| func FormatContentRange(start, length, completeLength int64) (hdr string, ok bool) {
 | |
| 	b := []byte("bytes ")
 | |
| 	switch {
 | |
| 	case start <= 0 && length <= 0 && completeLength >= 0: // i.e., unsatisfied-range
 | |
| 		b = append(b, "*/"...)
 | |
| 		b = strconv.AppendUint(b, uint64(completeLength), 10)
 | |
| 		ok = true
 | |
| 	case start >= 0 && length > 0: // i.e., byte-range "/" (complete-length | "*")
 | |
| 		b = strconv.AppendUint(b, uint64(start), 10)
 | |
| 		b = append(b, '-')
 | |
| 		b = strconv.AppendUint(b, uint64(start+length-1), 10)
 | |
| 		b = append(b, '/')
 | |
| 		if completeLength >= 0 {
 | |
| 			b = strconv.AppendUint(b, uint64(completeLength), 10)
 | |
| 			ok = completeLength >= start+length && start+length > 0
 | |
| 		} else {
 | |
| 			b = append(b, '*')
 | |
| 			ok = true
 | |
| 		}
 | |
| 	}
 | |
| 	return string(b), ok
 | |
| }
 | |
| 
 | |
| // parseNumber parses s as an unsigned decimal integer.
 | |
| // It parses according to the 1*DIGIT grammar, which allows leading zeros.
 | |
| func parseNumber(s string) (int64, bool) {
 | |
| 	suffix := strings.TrimLeft(s, "0123456789")
 | |
| 	prefix := s[:len(s)-len(suffix)]
 | |
| 	n, err := strconv.ParseInt(prefix, 10, 64)
 | |
| 	return n, suffix == "" && err == nil
 | |
| }
 |