| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- package httputil
- import (
- "bytes"
- "compress/gzip"
- "compress/zlib"
- "errors"
- "fmt"
- "io"
- "net/http"
- "strings"
- "github.com/klauspost/compress/zstd"
- )
- const (
- requestBodyReadInitCap = 512
- requestBodyReadMaxInitCap = 1 << 20
- jsonUTF8BOMLen = 3
- // maxDecompressedBodySize limits the decompressed request body to 64 MB
- // to prevent decompression bomb attacks.
- maxDecompressedBodySize = 64 << 20
- )
- // PrereadBody 回填已读取完成的请求体:作为 io.ReadCloser 可被再次顺序消费
- // (multipart 流式解析),同时暴露 Bytes() 让 ReadRequestBodyWithPrealloc
- // 直接返回原始切片,避免二次分配与复制。
- //
- // 注意:ReadRequestBodyWithPrealloc 对 PrereadBody 的快速路径不检查内部
- // reader 是否已被(部分)消费——包装的字节完整且不可变,即使 reader 已被
- // 流式消费过,Bytes() 也始终返回完整请求体。
- type PrereadBody struct {
- body []byte
- reader *bytes.Reader
- }
- // NewPrereadBody 包装一段已读取的请求体。
- func NewPrereadBody(body []byte) *PrereadBody {
- return &PrereadBody{body: body, reader: bytes.NewReader(body)}
- }
- // Read 实现 io.Reader(转发给内部 bytes.Reader)。
- func (p *PrereadBody) Read(b []byte) (int, error) {
- if p == nil {
- return 0, io.EOF
- }
- return p.reader.Read(b)
- }
- // Close 实现 io.Closer;请求体已在内存中,无需释放资源。
- func (p *PrereadBody) Close() error { return nil }
- // Bytes 返回完整的原始请求体切片。
- func (p *PrereadBody) Bytes() []byte {
- if p == nil {
- return nil
- }
- return p.body
- }
- // ReadRequestBodyWithPrealloc reads request body with preallocated buffer based
- // on content length, transparently decoding any Content-Encoding the upstream
- // client used to compress the body (zstd, gzip, deflate).
- // 已由 PrereadBody 回填的请求体直接返回其完整切片(零拷贝),不检查内部
- // reader 是否已被消费——见 PrereadBody 的文档说明。
- func ReadRequestBodyWithPrealloc(req *http.Request) ([]byte, error) {
- if req == nil || req.Body == nil {
- return nil, nil
- }
- if preread, ok := req.Body.(*PrereadBody); ok {
- return preread.Bytes(), nil
- }
- capHint := requestBodyReadInitCap
- if req.ContentLength > 0 {
- switch {
- case req.ContentLength < int64(requestBodyReadInitCap):
- capHint = requestBodyReadInitCap
- case req.ContentLength > int64(requestBodyReadMaxInitCap):
- capHint = requestBodyReadMaxInitCap
- default:
- capHint = int(req.ContentLength)
- }
- }
- buf := bytes.NewBuffer(make([]byte, 0, capHint))
- if _, err := io.Copy(buf, req.Body); err != nil {
- return nil, err
- }
- raw := buf.Bytes()
- enc := strings.ToLower(strings.TrimSpace(req.Header.Get("Content-Encoding")))
- if enc == "" || enc == "identity" {
- return raw, nil
- }
- decoded, err := decompressRequestBody(enc, raw)
- if err != nil {
- return nil, fmt.Errorf("decode Content-Encoding %q: %w", enc, err)
- }
- req.Header.Del("Content-Encoding")
- req.Header.Del("Content-Length")
- req.ContentLength = int64(len(decoded))
- return decoded, nil
- }
- // ReadLenientJSONRequestBodyWithPrealloc reads a request body and normalizes
- // JSON string control bytes before strict validation.
- func ReadLenientJSONRequestBodyWithPrealloc(req *http.Request, maxNormalizedBytes int64) ([]byte, error) {
- body, err := ReadRequestBodyWithPrealloc(req)
- if err != nil {
- return nil, err
- }
- return NormalizeLenientJSONRequestBody(body, maxNormalizedBytes)
- }
- func decompressRequestBody(encoding string, raw []byte) ([]byte, error) {
- switch encoding {
- case "zstd":
- dec, err := zstd.NewReader(bytes.NewReader(raw))
- if err != nil {
- return nil, err
- }
- defer dec.Close()
- return io.ReadAll(io.LimitReader(dec, maxDecompressedBodySize))
- case "gzip", "x-gzip":
- gr, err := gzip.NewReader(bytes.NewReader(raw))
- if err != nil {
- return nil, err
- }
- defer func() { _ = gr.Close() }()
- return io.ReadAll(io.LimitReader(gr, maxDecompressedBodySize))
- case "deflate":
- zr, err := zlib.NewReader(bytes.NewReader(raw))
- if err != nil {
- return nil, err
- }
- defer func() { _ = zr.Close() }()
- return io.ReadAll(io.LimitReader(zr, maxDecompressedBodySize))
- default:
- return nil, errors.New("unsupported Content-Encoding")
- }
- }
- // NormalizeLenientJSONRequestBody escapes raw control bytes that broken
- // OpenAI-compatible clients sometimes place inside JSON strings.
- func NormalizeLenientJSONRequestBody(body []byte, maxNormalizedBytes int64) ([]byte, error) {
- if maxNormalizedBytes <= 0 {
- maxNormalizedBytes = maxDecompressedBodySize
- }
- body = trimUTF8BOM(body)
- if len(body) == 0 {
- return body, nil
- }
- if int64(len(body)) > maxNormalizedBytes {
- return nil, &http.MaxBytesError{Limit: maxNormalizedBytes}
- }
- var out []byte
- inString := false
- escaped := false
- for i, b := range body {
- if inString && isJSONControlByte(b) {
- if out == nil {
- capHint := len(body) + 6
- if int64(capHint) > maxNormalizedBytes {
- capHint = int(maxNormalizedBytes)
- }
- out = make([]byte, 0, capHint)
- out = append(out, body[:i]...)
- }
- if int64(len(out)+6) > maxNormalizedBytes {
- return nil, &http.MaxBytesError{Limit: maxNormalizedBytes}
- }
- out = appendJSONUnicodeEscape(out, b)
- escaped = false
- continue
- }
- switch {
- case escaped:
- escaped = false
- case inString && b == '\\':
- escaped = true
- case b == '"':
- inString = !inString
- }
- if out != nil {
- if int64(len(out)+1) > maxNormalizedBytes {
- return nil, &http.MaxBytesError{Limit: maxNormalizedBytes}
- }
- out = append(out, b)
- }
- }
- if out != nil {
- return out, nil
- }
- return body, nil
- }
- func trimUTF8BOM(body []byte) []byte {
- if len(body) >= jsonUTF8BOMLen && body[0] == 0xef && body[1] == 0xbb && body[2] == 0xbf {
- return body[jsonUTF8BOMLen:]
- }
- return body
- }
- func isJSONControlByte(b byte) bool {
- return b < 0x20 || b == 0x7f
- }
- func appendJSONUnicodeEscape(dst []byte, b byte) []byte {
- const hex = "0123456789abcdef"
- return append(dst, '\\', 'u', '0', '0', hex[b>>4], hex[b&0x0f])
- }
|