230 lines
6.7 KiB
Go
230 lines
6.7 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package api
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// EncodingGzip is the gzip content encoding.
|
|
EncodingGzip = "gzip"
|
|
// EncodingIdentity means no encoding (passthrough).
|
|
EncodingIdentity = "identity"
|
|
// EncodingWildcard means any encoding is acceptable.
|
|
EncodingWildcard = "*"
|
|
)
|
|
|
|
// SupportedEncodings lists the content encodings supported for file transfer.
|
|
// The order matters - encodings are checked in order of preference.
|
|
var SupportedEncodings = []string{
|
|
EncodingGzip,
|
|
}
|
|
|
|
// encodingWithQuality holds an encoding name and its quality value.
|
|
type encodingWithQuality struct {
|
|
encoding string
|
|
quality float64
|
|
}
|
|
|
|
// isSupportedEncoding checks if the given encoding is in the supported list.
|
|
// Per RFC 7231, content-coding values are case-insensitive.
|
|
func isSupportedEncoding(encoding string) bool {
|
|
return slices.Contains(SupportedEncodings, strings.ToLower(encoding))
|
|
}
|
|
|
|
// parseEncodingWithQuality parses an encoding value and extracts the quality.
|
|
// Returns the encoding name (lowercased) and quality value (default 1.0 if not specified).
|
|
// Per RFC 7231, content-coding values are case-insensitive.
|
|
func parseEncodingWithQuality(value string) encodingWithQuality {
|
|
value = strings.TrimSpace(value)
|
|
quality := 1.0
|
|
|
|
if idx := strings.Index(value, ";"); idx != -1 {
|
|
params := value[idx+1:]
|
|
value = strings.TrimSpace(value[:idx])
|
|
|
|
// Parse q=X.X parameter
|
|
for param := range strings.SplitSeq(params, ";") {
|
|
param = strings.TrimSpace(param)
|
|
if strings.HasPrefix(strings.ToLower(param), "q=") {
|
|
if q, err := strconv.ParseFloat(param[2:], 64); err == nil {
|
|
quality = q
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Normalize encoding to lowercase per RFC 7231
|
|
return encodingWithQuality{encoding: strings.ToLower(value), quality: quality}
|
|
}
|
|
|
|
// parseEncoding extracts the encoding name from a header value, stripping quality.
|
|
func parseEncoding(value string) string {
|
|
return parseEncodingWithQuality(value).encoding
|
|
}
|
|
|
|
// parseContentEncoding parses the Content-Encoding header and returns the encoding.
|
|
// Returns an error if an unsupported encoding is specified.
|
|
// If no Content-Encoding header is present, returns empty string.
|
|
func parseContentEncoding(r *http.Request) (string, error) {
|
|
header := r.Header.Get("Content-Encoding")
|
|
if header == "" {
|
|
return EncodingIdentity, nil
|
|
}
|
|
|
|
encoding := parseEncoding(header)
|
|
|
|
if encoding == EncodingIdentity {
|
|
return EncodingIdentity, nil
|
|
}
|
|
|
|
if !isSupportedEncoding(encoding) {
|
|
return "", fmt.Errorf("unsupported Content-Encoding: %s, supported: %v", header, SupportedEncodings)
|
|
}
|
|
|
|
return encoding, nil
|
|
}
|
|
|
|
// parseAcceptEncodingHeader parses the Accept-Encoding header and returns
|
|
// the parsed encodings along with the identity rejection state.
|
|
// Per RFC 7231 Section 5.3.4, identity is acceptable unless excluded by
|
|
// "identity;q=0" or "*;q=0" without a more specific entry for identity with q>0.
|
|
func parseAcceptEncodingHeader(header string) ([]encodingWithQuality, bool) {
|
|
if header == "" {
|
|
return nil, false // identity not rejected when header is empty
|
|
}
|
|
|
|
// Parse all encodings with their quality values
|
|
var encodings []encodingWithQuality
|
|
for value := range strings.SplitSeq(header, ",") {
|
|
eq := parseEncodingWithQuality(value)
|
|
encodings = append(encodings, eq)
|
|
}
|
|
|
|
// Check if identity is rejected per RFC 7231 Section 5.3.4:
|
|
// identity is acceptable unless excluded by "identity;q=0" or "*;q=0"
|
|
// without a more specific entry for identity with q>0.
|
|
identityRejected := false
|
|
identityExplicitlyAccepted := false
|
|
wildcardRejected := false
|
|
|
|
for _, eq := range encodings {
|
|
switch eq.encoding {
|
|
case EncodingIdentity:
|
|
if eq.quality == 0 {
|
|
identityRejected = true
|
|
} else {
|
|
identityExplicitlyAccepted = true
|
|
}
|
|
case EncodingWildcard:
|
|
if eq.quality == 0 {
|
|
wildcardRejected = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if wildcardRejected && !identityExplicitlyAccepted {
|
|
identityRejected = true
|
|
}
|
|
|
|
return encodings, identityRejected
|
|
}
|
|
|
|
// isIdentityAcceptable checks if identity encoding is acceptable based on the
|
|
// Accept-Encoding header. Per RFC 7231 section 5.3.4, identity is always
|
|
// implicitly acceptable unless explicitly rejected with q=0.
|
|
func isIdentityAcceptable(r *http.Request) bool {
|
|
header := r.Header.Get("Accept-Encoding")
|
|
_, identityRejected := parseAcceptEncodingHeader(header)
|
|
|
|
return !identityRejected
|
|
}
|
|
|
|
// parseAcceptEncoding parses the Accept-Encoding header and returns the best
|
|
// supported encoding based on quality values. Per RFC 7231 section 5.3.4,
|
|
// identity is always implicitly acceptable unless explicitly rejected with q=0.
|
|
// If no Accept-Encoding header is present, returns empty string (identity).
|
|
func parseAcceptEncoding(r *http.Request) (string, error) {
|
|
header := r.Header.Get("Accept-Encoding")
|
|
if header == "" {
|
|
return EncodingIdentity, nil
|
|
}
|
|
|
|
encodings, identityRejected := parseAcceptEncodingHeader(header)
|
|
|
|
// Sort by quality value (highest first)
|
|
sort.Slice(encodings, func(i, j int) bool {
|
|
return encodings[i].quality > encodings[j].quality
|
|
})
|
|
|
|
// Find the best supported encoding
|
|
for _, eq := range encodings {
|
|
// Skip encodings with q=0 (explicitly rejected)
|
|
if eq.quality == 0 {
|
|
continue
|
|
}
|
|
|
|
if eq.encoding == EncodingIdentity {
|
|
return EncodingIdentity, nil
|
|
}
|
|
|
|
// Wildcard means any encoding is acceptable - return a supported encoding if identity is rejected
|
|
if eq.encoding == EncodingWildcard {
|
|
if identityRejected && len(SupportedEncodings) > 0 {
|
|
return SupportedEncodings[0], nil
|
|
}
|
|
|
|
return EncodingIdentity, nil
|
|
}
|
|
|
|
if isSupportedEncoding(eq.encoding) {
|
|
return eq.encoding, nil
|
|
}
|
|
}
|
|
|
|
// Per RFC 7231, identity is implicitly acceptable unless rejected
|
|
if !identityRejected {
|
|
return EncodingIdentity, nil
|
|
}
|
|
|
|
// Identity rejected and no supported encodings found
|
|
return "", fmt.Errorf("no acceptable encoding found, supported: %v", SupportedEncodings)
|
|
}
|
|
|
|
// getDecompressedBody returns a reader that decompresses the request body based on
|
|
// Content-Encoding header. Returns the original body if no encoding is specified.
|
|
// Returns an error if an unsupported encoding is specified.
|
|
// The caller is responsible for closing both the returned ReadCloser and the
|
|
// original request body (r.Body) separately.
|
|
func getDecompressedBody(r *http.Request) (io.ReadCloser, error) {
|
|
encoding, err := parseContentEncoding(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if encoding == EncodingIdentity {
|
|
return r.Body, nil
|
|
}
|
|
|
|
switch encoding {
|
|
case EncodingGzip:
|
|
gzReader, err := gzip.NewReader(r.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
|
|
}
|
|
|
|
return gzReader, nil
|
|
default:
|
|
// This shouldn't happen if isSupportedEncoding is correct
|
|
return nil, fmt.Errorf("encoding %s is supported but not implemented", encoding)
|
|
}
|
|
}
|