179 lines
4.2 KiB
Go
179 lines
4.2 KiB
Go
package contentcoding
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"compress/zlib"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"strings"
|
|
|
|
"github.com/andybalholm/brotli"
|
|
"github.com/klauspost/compress/zstd"
|
|
)
|
|
|
|
type Encoding string
|
|
|
|
const (
|
|
Identity Encoding = "identity"
|
|
Gzip Encoding = "gzip"
|
|
Brotli Encoding = "br"
|
|
Deflate Encoding = "deflate"
|
|
Zstd Encoding = "zstd"
|
|
)
|
|
|
|
const (
|
|
SupportedRequestEncodings = "identity, gzip, br, deflate, zstd"
|
|
ZstdHTTPMaxWindowBytes = 8 << 20
|
|
)
|
|
|
|
var (
|
|
ErrUnsupportedContentEncoding = errors.New("unsupported content encoding")
|
|
ErrInvalidContentEncoding = errors.New("invalid content encoding")
|
|
ErrDecodedBodyTooLarge = errors.New("decoded body exceeds size limit")
|
|
)
|
|
|
|
func ParseContentEncoding(values []string) (Encoding, error) {
|
|
if len(values) == 0 {
|
|
return Identity, nil
|
|
}
|
|
if len(values) != 1 {
|
|
return "", ErrUnsupportedContentEncoding
|
|
}
|
|
|
|
value := strings.ToLower(strings.TrimSpace(values[0]))
|
|
if value == "" || value == string(Identity) {
|
|
return Identity, nil
|
|
}
|
|
if strings.Contains(value, ",") {
|
|
return "", ErrUnsupportedContentEncoding
|
|
}
|
|
|
|
switch Encoding(value) {
|
|
case Gzip, Brotli, Deflate, Zstd:
|
|
return Encoding(value), nil
|
|
default:
|
|
return "", ErrUnsupportedContentEncoding
|
|
}
|
|
}
|
|
|
|
func DecodeLimited(encoding Encoding, encoded []byte, limit int64) ([]byte, error) {
|
|
if limit < 0 {
|
|
return nil, fmt.Errorf("decoded body limit must not be negative")
|
|
}
|
|
|
|
switch encoding {
|
|
case Identity:
|
|
return readAtMost(bytes.NewReader(encoded), limit)
|
|
case Gzip:
|
|
if len(encoded) == 0 {
|
|
return nil, ErrInvalidContentEncoding
|
|
}
|
|
return decodeGzip(encoded, limit)
|
|
case Brotli:
|
|
if len(encoded) == 0 {
|
|
return nil, ErrInvalidContentEncoding
|
|
}
|
|
return readDecoded(brotli.NewReader(bytes.NewReader(encoded)), limit)
|
|
case Deflate:
|
|
if len(encoded) == 0 {
|
|
return nil, ErrInvalidContentEncoding
|
|
}
|
|
return decodeDeflate(encoded, limit)
|
|
case Zstd:
|
|
if len(encoded) == 0 {
|
|
return nil, ErrInvalidContentEncoding
|
|
}
|
|
return decodeZstd(encoded, limit)
|
|
default:
|
|
return nil, ErrUnsupportedContentEncoding
|
|
}
|
|
}
|
|
|
|
func decodeGzip(encoded []byte, limit int64) ([]byte, error) {
|
|
source := bytes.NewReader(encoded)
|
|
reader, err := gzip.NewReader(source)
|
|
if err != nil {
|
|
return nil, invalidContentEncoding(err)
|
|
}
|
|
reader.Multistream(true)
|
|
|
|
body, readErr := readDecoded(reader, limit)
|
|
closeErr := reader.Close()
|
|
if readErr != nil {
|
|
return nil, readErr
|
|
}
|
|
if closeErr != nil {
|
|
return nil, invalidContentEncoding(closeErr)
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
func decodeDeflate(encoded []byte, limit int64) ([]byte, error) {
|
|
source := bytes.NewReader(encoded)
|
|
reader, err := zlib.NewReader(source)
|
|
if err != nil {
|
|
return nil, invalidContentEncoding(err)
|
|
}
|
|
body, readErr := readDecoded(reader, limit)
|
|
closeErr := reader.Close()
|
|
if readErr != nil {
|
|
return nil, readErr
|
|
}
|
|
if closeErr != nil {
|
|
return nil, invalidContentEncoding(closeErr)
|
|
}
|
|
if source.Len() != 0 {
|
|
return nil, ErrInvalidContentEncoding
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
func decodeZstd(encoded []byte, limit int64) ([]byte, error) {
|
|
memoryLimit := uint64(max(limit, int64(ZstdHTTPMaxWindowBytes)))
|
|
reader, err := zstd.NewReader(
|
|
bytes.NewReader(encoded),
|
|
zstd.WithDecoderConcurrency(1),
|
|
zstd.WithDecoderLowmem(true),
|
|
zstd.WithDecoderMaxMemory(memoryLimit),
|
|
zstd.WithDecoderMaxWindow(ZstdHTTPMaxWindowBytes),
|
|
)
|
|
if err != nil {
|
|
return nil, invalidContentEncoding(err)
|
|
}
|
|
defer reader.Close()
|
|
return readDecoded(reader, limit)
|
|
}
|
|
|
|
func readDecoded(reader io.Reader, limit int64) ([]byte, error) {
|
|
body, err := readAtMost(reader, limit)
|
|
if err == nil || errors.Is(err, ErrDecodedBodyTooLarge) {
|
|
return body, err
|
|
}
|
|
return nil, invalidContentEncoding(err)
|
|
}
|
|
|
|
func readAtMost(reader io.Reader, limit int64) ([]byte, error) {
|
|
if limit < 0 {
|
|
return nil, fmt.Errorf("decoded body limit must not be negative")
|
|
}
|
|
limited := io.Reader(reader)
|
|
if limit < math.MaxInt64 {
|
|
limited = io.LimitReader(reader, limit+1)
|
|
}
|
|
body, err := io.ReadAll(limited)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if int64(len(body)) > limit {
|
|
return nil, ErrDecodedBodyTooLarge
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
func invalidContentEncoding(err error) error {
|
|
return fmt.Errorf("%w: %v", ErrInvalidContentEncoding, err)
|
|
}
|