body.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. package httputil
  2. import (
  3. "bytes"
  4. "compress/gzip"
  5. "compress/zlib"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "strings"
  11. "github.com/klauspost/compress/zstd"
  12. )
  13. const (
  14. requestBodyReadInitCap = 512
  15. requestBodyReadMaxInitCap = 1 << 20
  16. jsonUTF8BOMLen = 3
  17. // maxDecompressedBodySize limits the decompressed request body to 64 MB
  18. // to prevent decompression bomb attacks.
  19. maxDecompressedBodySize = 64 << 20
  20. )
  21. // PrereadBody 回填已读取完成的请求体:作为 io.ReadCloser 可被再次顺序消费
  22. // (multipart 流式解析),同时暴露 Bytes() 让 ReadRequestBodyWithPrealloc
  23. // 直接返回原始切片,避免二次分配与复制。
  24. //
  25. // 注意:ReadRequestBodyWithPrealloc 对 PrereadBody 的快速路径不检查内部
  26. // reader 是否已被(部分)消费——包装的字节完整且不可变,即使 reader 已被
  27. // 流式消费过,Bytes() 也始终返回完整请求体。
  28. type PrereadBody struct {
  29. body []byte
  30. reader *bytes.Reader
  31. }
  32. // NewPrereadBody 包装一段已读取的请求体。
  33. func NewPrereadBody(body []byte) *PrereadBody {
  34. return &PrereadBody{body: body, reader: bytes.NewReader(body)}
  35. }
  36. // Read 实现 io.Reader(转发给内部 bytes.Reader)。
  37. func (p *PrereadBody) Read(b []byte) (int, error) {
  38. if p == nil {
  39. return 0, io.EOF
  40. }
  41. return p.reader.Read(b)
  42. }
  43. // Close 实现 io.Closer;请求体已在内存中,无需释放资源。
  44. func (p *PrereadBody) Close() error { return nil }
  45. // Bytes 返回完整的原始请求体切片。
  46. func (p *PrereadBody) Bytes() []byte {
  47. if p == nil {
  48. return nil
  49. }
  50. return p.body
  51. }
  52. // ReadRequestBodyWithPrealloc reads request body with preallocated buffer based
  53. // on content length, transparently decoding any Content-Encoding the upstream
  54. // client used to compress the body (zstd, gzip, deflate).
  55. // 已由 PrereadBody 回填的请求体直接返回其完整切片(零拷贝),不检查内部
  56. // reader 是否已被消费——见 PrereadBody 的文档说明。
  57. func ReadRequestBodyWithPrealloc(req *http.Request) ([]byte, error) {
  58. if req == nil || req.Body == nil {
  59. return nil, nil
  60. }
  61. if preread, ok := req.Body.(*PrereadBody); ok {
  62. return preread.Bytes(), nil
  63. }
  64. capHint := requestBodyReadInitCap
  65. if req.ContentLength > 0 {
  66. switch {
  67. case req.ContentLength < int64(requestBodyReadInitCap):
  68. capHint = requestBodyReadInitCap
  69. case req.ContentLength > int64(requestBodyReadMaxInitCap):
  70. capHint = requestBodyReadMaxInitCap
  71. default:
  72. capHint = int(req.ContentLength)
  73. }
  74. }
  75. buf := bytes.NewBuffer(make([]byte, 0, capHint))
  76. if _, err := io.Copy(buf, req.Body); err != nil {
  77. return nil, err
  78. }
  79. raw := buf.Bytes()
  80. enc := strings.ToLower(strings.TrimSpace(req.Header.Get("Content-Encoding")))
  81. if enc == "" || enc == "identity" {
  82. return raw, nil
  83. }
  84. decoded, err := decompressRequestBody(enc, raw)
  85. if err != nil {
  86. return nil, fmt.Errorf("decode Content-Encoding %q: %w", enc, err)
  87. }
  88. req.Header.Del("Content-Encoding")
  89. req.Header.Del("Content-Length")
  90. req.ContentLength = int64(len(decoded))
  91. return decoded, nil
  92. }
  93. // ReadLenientJSONRequestBodyWithPrealloc reads a request body and normalizes
  94. // JSON string control bytes before strict validation.
  95. func ReadLenientJSONRequestBodyWithPrealloc(req *http.Request, maxNormalizedBytes int64) ([]byte, error) {
  96. body, err := ReadRequestBodyWithPrealloc(req)
  97. if err != nil {
  98. return nil, err
  99. }
  100. return NormalizeLenientJSONRequestBody(body, maxNormalizedBytes)
  101. }
  102. func decompressRequestBody(encoding string, raw []byte) ([]byte, error) {
  103. switch encoding {
  104. case "zstd":
  105. dec, err := zstd.NewReader(bytes.NewReader(raw))
  106. if err != nil {
  107. return nil, err
  108. }
  109. defer dec.Close()
  110. return io.ReadAll(io.LimitReader(dec, maxDecompressedBodySize))
  111. case "gzip", "x-gzip":
  112. gr, err := gzip.NewReader(bytes.NewReader(raw))
  113. if err != nil {
  114. return nil, err
  115. }
  116. defer func() { _ = gr.Close() }()
  117. return io.ReadAll(io.LimitReader(gr, maxDecompressedBodySize))
  118. case "deflate":
  119. zr, err := zlib.NewReader(bytes.NewReader(raw))
  120. if err != nil {
  121. return nil, err
  122. }
  123. defer func() { _ = zr.Close() }()
  124. return io.ReadAll(io.LimitReader(zr, maxDecompressedBodySize))
  125. default:
  126. return nil, errors.New("unsupported Content-Encoding")
  127. }
  128. }
  129. // NormalizeLenientJSONRequestBody escapes raw control bytes that broken
  130. // OpenAI-compatible clients sometimes place inside JSON strings.
  131. func NormalizeLenientJSONRequestBody(body []byte, maxNormalizedBytes int64) ([]byte, error) {
  132. if maxNormalizedBytes <= 0 {
  133. maxNormalizedBytes = maxDecompressedBodySize
  134. }
  135. body = trimUTF8BOM(body)
  136. if len(body) == 0 {
  137. return body, nil
  138. }
  139. if int64(len(body)) > maxNormalizedBytes {
  140. return nil, &http.MaxBytesError{Limit: maxNormalizedBytes}
  141. }
  142. var out []byte
  143. inString := false
  144. escaped := false
  145. for i, b := range body {
  146. if inString && isJSONControlByte(b) {
  147. if out == nil {
  148. capHint := len(body) + 6
  149. if int64(capHint) > maxNormalizedBytes {
  150. capHint = int(maxNormalizedBytes)
  151. }
  152. out = make([]byte, 0, capHint)
  153. out = append(out, body[:i]...)
  154. }
  155. if int64(len(out)+6) > maxNormalizedBytes {
  156. return nil, &http.MaxBytesError{Limit: maxNormalizedBytes}
  157. }
  158. out = appendJSONUnicodeEscape(out, b)
  159. escaped = false
  160. continue
  161. }
  162. switch {
  163. case escaped:
  164. escaped = false
  165. case inString && b == '\\':
  166. escaped = true
  167. case b == '"':
  168. inString = !inString
  169. }
  170. if out != nil {
  171. if int64(len(out)+1) > maxNormalizedBytes {
  172. return nil, &http.MaxBytesError{Limit: maxNormalizedBytes}
  173. }
  174. out = append(out, b)
  175. }
  176. }
  177. if out != nil {
  178. return out, nil
  179. }
  180. return body, nil
  181. }
  182. func trimUTF8BOM(body []byte) []byte {
  183. if len(body) >= jsonUTF8BOMLen && body[0] == 0xef && body[1] == 0xbb && body[2] == 0xbf {
  184. return body[jsonUTF8BOMLen:]
  185. }
  186. return body
  187. }
  188. func isJSONControlByte(b byte) bool {
  189. return b < 0x20 || b == 0x7f
  190. }
  191. func appendJSONUnicodeEscape(dst []byte, b byte) []byte {
  192. const hex = "0123456789abcdef"
  193. return append(dst, '\\', 'u', '0', '0', hex[b>>4], hex[b&0x0f])
  194. }