|
| 1 | +// Copyright (c) Tailscale Inc & AUTHORS |
| 2 | +// SPDX-License-Identifier: BSD-3-Clause |
| 3 | + |
| 4 | +// Package httphdr implements functionality for parsing and formatting |
| 5 | +// standard HTTP headers. |
| 6 | +package httphdr |
| 7 | + |
| 8 | +import ( |
| 9 | + "bytes" |
| 10 | + "strconv" |
| 11 | + "strings" |
| 12 | +) |
| 13 | + |
| 14 | +// Range is a range of bytes within some content. |
| 15 | +type Range struct { |
| 16 | + // Start is the starting offset. |
| 17 | + // It is zero if Length is negative; it must not be negative. |
| 18 | + Start int64 |
| 19 | + // Length is the length of the content. |
| 20 | + // It is zero if the length extends to the end of the content. |
| 21 | + // It is negative if the length is relative to the end (e.g., last 5 bytes). |
| 22 | + Length int64 |
| 23 | +} |
| 24 | + |
| 25 | +// ows is optional whitespace. |
| 26 | +const ows = " \t" // per RFC 7230, section 3.2.3 |
| 27 | + |
| 28 | +// ParseRange parses a "Range" header per RFC 7233, section 3. |
| 29 | +// It only handles "Range" headers where the units is "bytes". |
| 30 | +// The "Range" header is usually only specified in GET requests. |
| 31 | +func ParseRange(hdr string) (ranges []Range, ok bool) { |
| 32 | + // Grammar per RFC 7233, appendix D: |
| 33 | + // Range = byte-ranges-specifier | other-ranges-specifier |
| 34 | + // byte-ranges-specifier = bytes-unit "=" byte-range-set |
| 35 | + // bytes-unit = "bytes" |
| 36 | + // byte-range-set = |
| 37 | + // *("," OWS) |
| 38 | + // (byte-range-spec | suffix-byte-range-spec) |
| 39 | + // *(OWS "," [OWS ( byte-range-spec | suffix-byte-range-spec )]) |
| 40 | + // byte-range-spec = first-byte-pos "-" [last-byte-pos] |
| 41 | + // suffix-byte-range-spec = "-" suffix-length |
| 42 | + // We do not support other-ranges-specifier. |
| 43 | + // All other identifiers are 1*DIGIT. |
| 44 | + hdr = strings.Trim(hdr, ows) // per RFC 7230, section 3.2 |
| 45 | + units, elems, hasUnits := strings.Cut(hdr, "=") |
| 46 | + elems = strings.TrimLeft(elems, ","+ows) |
| 47 | + for _, elem := range strings.Split(elems, ",") { |
| 48 | + elem = strings.Trim(elem, ows) // per RFC 7230, section 7 |
| 49 | + switch { |
| 50 | + case strings.HasPrefix(elem, "-"): // i.e., "-" suffix-length |
| 51 | + n, ok := parseNumber(strings.TrimPrefix(elem, "-")) |
| 52 | + if !ok { |
| 53 | + return ranges, false |
| 54 | + } |
| 55 | + ranges = append(ranges, Range{0, -n}) |
| 56 | + case strings.HasSuffix(elem, "-"): // i.e., first-byte-pos "-" |
| 57 | + n, ok := parseNumber(strings.TrimSuffix(elem, "-")) |
| 58 | + if !ok { |
| 59 | + return ranges, false |
| 60 | + } |
| 61 | + ranges = append(ranges, Range{n, 0}) |
| 62 | + default: // i.e., first-byte-pos "-" last-byte-pos |
| 63 | + prefix, suffix, hasDash := strings.Cut(elem, "-") |
| 64 | + n, ok2 := parseNumber(prefix) |
| 65 | + m, ok3 := parseNumber(suffix) |
| 66 | + if !hasDash || !ok2 || !ok3 || m < n { |
| 67 | + return ranges, false |
| 68 | + } |
| 69 | + ranges = append(ranges, Range{n, m - n + 1}) |
| 70 | + } |
| 71 | + } |
| 72 | + return ranges, units == "bytes" && hasUnits && len(ranges) > 0 // must see at least one element per RFC 7233, section 2.1 |
| 73 | +} |
| 74 | + |
| 75 | +// FormatRange formats a "Range" header per RFC 7233, section 3. |
| 76 | +// It only handles "Range" headers where the units is "bytes". |
| 77 | +// The "Range" header is usually only specified in GET requests. |
| 78 | +func FormatRange(ranges []Range) (hdr string, ok bool) { |
| 79 | + b := []byte("bytes=") |
| 80 | + for _, r := range ranges { |
| 81 | + switch { |
| 82 | + case r.Length > 0: // i.e., first-byte-pos "-" last-byte-pos |
| 83 | + if r.Start < 0 { |
| 84 | + return string(b), false |
| 85 | + } |
| 86 | + b = strconv.AppendUint(b, uint64(r.Start), 10) |
| 87 | + b = append(b, '-') |
| 88 | + b = strconv.AppendUint(b, uint64(r.Start+r.Length-1), 10) |
| 89 | + b = append(b, ',') |
| 90 | + case r.Length == 0: // i.e., first-byte-pos "-" |
| 91 | + if r.Start < 0 { |
| 92 | + return string(b), false |
| 93 | + } |
| 94 | + b = strconv.AppendUint(b, uint64(r.Start), 10) |
| 95 | + b = append(b, '-') |
| 96 | + b = append(b, ',') |
| 97 | + case r.Length < 0: // i.e., "-" suffix-length |
| 98 | + if r.Start != 0 { |
| 99 | + return string(b), false |
| 100 | + } |
| 101 | + b = append(b, '-') |
| 102 | + b = strconv.AppendUint(b, uint64(-r.Length), 10) |
| 103 | + b = append(b, ',') |
| 104 | + default: |
| 105 | + return string(b), false |
| 106 | + } |
| 107 | + } |
| 108 | + return string(bytes.TrimRight(b, ",")), len(ranges) > 0 |
| 109 | +} |
| 110 | + |
| 111 | +// ParseContentRange parses a "Content-Range" header per RFC 7233, section 4.2. |
| 112 | +// It only handles "Content-Range" headers where the units is "bytes". |
| 113 | +// The "Content-Range" header is usually only specified in HTTP responses. |
| 114 | +// |
| 115 | +// If only the completeLength is specified, then start and length are both zero. |
| 116 | +// |
| 117 | +// Otherwise, the parses the start and length and the optional completeLength, |
| 118 | +// which is -1 if unspecified. The start is non-negative and the length is positive. |
| 119 | +func ParseContentRange(hdr string) (start, length, completeLength int64, ok bool) { |
| 120 | + // Grammar per RFC 7233, appendix D: |
| 121 | + // Content-Range = byte-content-range | other-content-range |
| 122 | + // byte-content-range = bytes-unit SP (byte-range-resp | unsatisfied-range) |
| 123 | + // bytes-unit = "bytes" |
| 124 | + // byte-range-resp = byte-range "/" (complete-length | "*") |
| 125 | + // unsatisfied-range = "*/" complete-length |
| 126 | + // byte-range = first-byte-pos "-" last-byte-pos |
| 127 | + // We do not support other-content-range. |
| 128 | + // All other identifiers are 1*DIGIT. |
| 129 | + hdr = strings.Trim(hdr, ows) // per RFC 7230, section 3.2 |
| 130 | + suffix, hasUnits := strings.CutPrefix(hdr, "bytes ") |
| 131 | + suffix, unsatisfied := strings.CutPrefix(suffix, "*/") |
| 132 | + if unsatisfied { // i.e., unsatisfied-range |
| 133 | + n, ok := parseNumber(suffix) |
| 134 | + if !ok { |
| 135 | + return start, length, completeLength, false |
| 136 | + } |
| 137 | + completeLength = n |
| 138 | + } else { // i.e., byte-range "/" (complete-length | "*") |
| 139 | + prefix, suffix, hasDash := strings.Cut(suffix, "-") |
| 140 | + middle, suffix, hasSlash := strings.Cut(suffix, "/") |
| 141 | + n, ok0 := parseNumber(prefix) |
| 142 | + m, ok1 := parseNumber(middle) |
| 143 | + o, ok2 := parseNumber(suffix) |
| 144 | + if suffix == "*" { |
| 145 | + o, ok2 = -1, true |
| 146 | + } |
| 147 | + if !hasDash || !hasSlash || !ok0 || !ok1 || !ok2 || m < n || (o >= 0 && o <= m) { |
| 148 | + return start, length, completeLength, false |
| 149 | + } |
| 150 | + start = n |
| 151 | + length = m - n + 1 |
| 152 | + completeLength = o |
| 153 | + } |
| 154 | + return start, length, completeLength, hasUnits |
| 155 | +} |
| 156 | + |
| 157 | +// FormatContentRange parses a "Content-Range" header per RFC 7233, section 4.2. |
| 158 | +// It only handles "Content-Range" headers where the units is "bytes". |
| 159 | +// The "Content-Range" header is usually only specified in HTTP responses. |
| 160 | +// |
| 161 | +// If start and length are non-positive, then it encodes just the completeLength, |
| 162 | +// which must be a non-negative value. |
| 163 | +// |
| 164 | +// Otherwise, it encodes the start and length as a byte-range, |
| 165 | +// and optionally emits the complete length if it is non-negative. |
| 166 | +// The length must be positive (as RFC 7233 uses inclusive end offsets). |
| 167 | +func FormatContentRange(start, length, completeLength int64) (hdr string, ok bool) { |
| 168 | + b := []byte("bytes ") |
| 169 | + switch { |
| 170 | + case start <= 0 && length <= 0 && completeLength >= 0: // i.e., unsatisfied-range |
| 171 | + b = append(b, "*/"...) |
| 172 | + b = strconv.AppendUint(b, uint64(completeLength), 10) |
| 173 | + ok = true |
| 174 | + case start >= 0 && length > 0: // i.e., byte-range "/" (complete-length | "*") |
| 175 | + b = strconv.AppendUint(b, uint64(start), 10) |
| 176 | + b = append(b, '-') |
| 177 | + b = strconv.AppendUint(b, uint64(start+length-1), 10) |
| 178 | + b = append(b, '/') |
| 179 | + if completeLength >= 0 { |
| 180 | + b = strconv.AppendUint(b, uint64(completeLength), 10) |
| 181 | + ok = completeLength >= start+length && start+length > 0 |
| 182 | + } else { |
| 183 | + b = append(b, '*') |
| 184 | + ok = true |
| 185 | + } |
| 186 | + } |
| 187 | + return string(b), ok |
| 188 | +} |
| 189 | + |
| 190 | +// parseNumber parses s as an unsigned decimal integer. |
| 191 | +// It parses according to the 1*DIGIT grammar, which allows leading zeros. |
| 192 | +func parseNumber(s string) (int64, bool) { |
| 193 | + suffix := strings.TrimLeft(s, "0123456789") |
| 194 | + prefix := s[:len(s)-len(suffix)] |
| 195 | + n, err := strconv.ParseInt(prefix, 10, 64) |
| 196 | + return n, suffix == "" && err == nil |
| 197 | +} |
0 commit comments