82 lines
2.9 KiB
Go
82 lines
2.9 KiB
Go
package util
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/open-policy-agent/opa/util/decoding"
|
|
)
|
|
|
|
var gzipReaderPool = sync.Pool{
|
|
New: func() interface{} {
|
|
reader := new(gzip.Reader)
|
|
return reader
|
|
},
|
|
}
|
|
|
|
// Note(philipc): Originally taken from server/server.go
|
|
// The DecodingLimitHandler handles validating that the gzip payload is within the
|
|
// allowed max size limit. Thus, in the event of a forged payload size trailer,
|
|
// the worst that can happen is that we waste memory up to the allowed max gzip
|
|
// payload size, but not an unbounded amount of memory, as was potentially
|
|
// possible before.
|
|
func ReadMaybeCompressedBody(r *http.Request) ([]byte, error) {
|
|
var content *bytes.Buffer
|
|
// Note(philipc): If the request body is of unknown length (such as what
|
|
// happens when 'Transfer-Encoding: chunked' is set), we have to do an
|
|
// incremental read of the body. In this case, we can't be too clever, we
|
|
// just do the best we can with whatever is streamed over to us.
|
|
// Fetch gzip payload size limit from request context.
|
|
if maxLength, ok := decoding.GetServerDecodingMaxLen(r.Context()); ok {
|
|
bs, err := io.ReadAll(io.LimitReader(r.Body, maxLength))
|
|
if err != nil {
|
|
return bs, err
|
|
}
|
|
content = bytes.NewBuffer(bs)
|
|
} else {
|
|
// Read content from the request body into a buffer of known size.
|
|
content = bytes.NewBuffer(make([]byte, 0, r.ContentLength))
|
|
if _, err := io.CopyN(content, r.Body, r.ContentLength); err != nil {
|
|
return content.Bytes(), err
|
|
}
|
|
}
|
|
|
|
// Decompress gzip content by reading from the buffer.
|
|
if strings.Contains(r.Header.Get("Content-Encoding"), "gzip") {
|
|
// Fetch gzip payload size limit from request context.
|
|
gzipMaxLength, _ := decoding.GetServerDecodingGzipMaxLen(r.Context())
|
|
|
|
// Note(philipc): The last 4 bytes of a well-formed gzip blob will
|
|
// always be a little-endian uint32, representing the decompressed
|
|
// content size, modulo 2^32. We validate that the size is safe,
|
|
// earlier in DecodingLimitHandler.
|
|
sizeTrailerField := binary.LittleEndian.Uint32(content.Bytes()[content.Len()-4:])
|
|
if sizeTrailerField > uint32(gzipMaxLength) {
|
|
return content.Bytes(), fmt.Errorf("gzip payload too large")
|
|
}
|
|
// Pull a gzip decompressor from the pool, and assign it to the current
|
|
// buffer, using Reset(). Later, return it back to the pool for another
|
|
// request to use.
|
|
gzReader := gzipReaderPool.Get().(*gzip.Reader)
|
|
if err := gzReader.Reset(content); err != nil {
|
|
return nil, err
|
|
}
|
|
defer gzReader.Close()
|
|
defer gzipReaderPool.Put(gzReader)
|
|
decompressedContent := bytes.NewBuffer(make([]byte, 0, sizeTrailerField))
|
|
if _, err := io.CopyN(decompressedContent, gzReader, int64(sizeTrailerField)); err != nil {
|
|
return decompressedContent.Bytes(), err
|
|
}
|
|
return decompressedContent.Bytes(), nil
|
|
}
|
|
|
|
// Request was not compressed; return the content bytes.
|
|
return content.Bytes(), nil
|
|
}
|