feat: support bounded chunked request bodies
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
//
|
||||
// It deliberately keeps the public handler boundary as net/http so existing
|
||||
// GoOmni business code remains unchanged while connection management and
|
||||
// protocol I/O move out of net/http.Server. The parser currently supports
|
||||
// Content-Length request bodies and rejects chunked requests.
|
||||
// protocol I/O move out of net/http.Server. The parser supports Content-Length
|
||||
// and chunked request bodies with configurable size limits.
|
||||
package native
|
||||
|
||||
import (
|
||||
@@ -493,14 +493,23 @@ func readRequest(reader *bufio.Reader, conn net.Conn, maxHeaderBytes int, maxBod
|
||||
return nil, true, &protocolError{status: http.StatusExpectationFailed, message: "unsupported expectation"}
|
||||
}
|
||||
}
|
||||
chunked := false
|
||||
for _, value := range header.Values("Transfer-Encoding") {
|
||||
for _, coding := range strings.Split(value, ",") {
|
||||
coding = strings.TrimSpace(coding)
|
||||
if coding != "" && !strings.EqualFold(coding, "identity") {
|
||||
return nil, true, &protocolError{status: http.StatusNotImplemented, message: "transfer encoding is not supported"}
|
||||
if coding == "" || strings.EqualFold(coding, "identity") {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(coding, "chunked") && !chunked {
|
||||
chunked = true
|
||||
continue
|
||||
}
|
||||
return nil, true, &protocolError{status: http.StatusNotImplemented, message: "transfer encoding is not supported"}
|
||||
}
|
||||
}
|
||||
if chunked && len(header.Values("Content-Length")) > 0 {
|
||||
return nil, true, &protocolError{status: http.StatusBadRequest, message: "content length with chunked encoding"}
|
||||
}
|
||||
contentLength := int64(0)
|
||||
contentLengthSeen := false
|
||||
for _, value := range header.Values("Content-Length") {
|
||||
@@ -514,17 +523,31 @@ func readRequest(reader *bufio.Reader, conn net.Conn, maxHeaderBytes int, maxBod
|
||||
contentLengthSeen = true
|
||||
}
|
||||
}
|
||||
if contentLength > maxBodyBytes {
|
||||
if !chunked && contentLength > maxBodyBytes {
|
||||
return nil, true, &protocolError{status: http.StatusRequestEntityTooLarge, message: "request body too large"}
|
||||
}
|
||||
body := make([]byte, contentLength)
|
||||
if contentLength > 0 {
|
||||
if setBodyDeadline != nil {
|
||||
setBodyDeadline()
|
||||
}
|
||||
if _, err := io.ReadFull(reader, body); err != nil {
|
||||
var body []byte
|
||||
var trailer http.Header
|
||||
if chunked {
|
||||
body, trailer, err = readChunkedBody(reader, maxBodyBytes, setBodyDeadline, readLine)
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
// net/http uses -1 to indicate that the decoded length was not known
|
||||
// from the request headers. The body is already buffered for the
|
||||
// handler, but retaining this value keeps the request contract
|
||||
// compatible with the standard server.
|
||||
contentLength = -1
|
||||
} else {
|
||||
body = make([]byte, contentLength)
|
||||
if contentLength > 0 {
|
||||
if setBodyDeadline != nil {
|
||||
setBodyDeadline()
|
||||
}
|
||||
if _, err := io.ReadFull(reader, body); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
}
|
||||
}
|
||||
request := &http.Request{
|
||||
Method: parts[0],
|
||||
@@ -538,6 +561,10 @@ func readRequest(reader *bufio.Reader, conn net.Conn, maxHeaderBytes int, maxBod
|
||||
Host: header.Get("Host"),
|
||||
RequestURI: target,
|
||||
RemoteAddr: conn.RemoteAddr().String(),
|
||||
Trailer: trailer,
|
||||
}
|
||||
if chunked {
|
||||
request.TransferEncoding = []string{"chunked"}
|
||||
}
|
||||
closeAfter := headerHasToken(header.Values("Connection"), "close") ||
|
||||
(protoMajor == 1 && protoMinor == 0 && !headerHasToken(header.Values("Connection"), "keep-alive"))
|
||||
@@ -545,6 +572,90 @@ func readRequest(reader *bufio.Reader, conn net.Conn, maxHeaderBytes int, maxBod
|
||||
return request, closeAfter, nil
|
||||
}
|
||||
|
||||
func readChunkedBody(reader *bufio.Reader, maxBodyBytes int64, setBodyDeadline func(), readLine func() (string, error)) ([]byte, http.Header, error) {
|
||||
var body bytes.Buffer
|
||||
initialCapacity := int64(64 << 10)
|
||||
if maxBodyBytes < initialCapacity {
|
||||
initialCapacity = maxBodyBytes
|
||||
}
|
||||
if initialCapacity > 0 && initialCapacity <= int64(int(^uint(0)>>1)) {
|
||||
body.Grow(int(initialCapacity))
|
||||
}
|
||||
|
||||
for {
|
||||
line, err := readLine()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
chunkSizeText := line
|
||||
if extension := strings.IndexByte(chunkSizeText, ';'); extension >= 0 {
|
||||
chunkSizeText = chunkSizeText[:extension]
|
||||
}
|
||||
chunkSizeText = strings.TrimSpace(chunkSizeText)
|
||||
if chunkSizeText == "" || !validHexNumber(chunkSizeText) {
|
||||
return nil, nil, &protocolError{status: http.StatusBadRequest, message: "invalid chunk size"}
|
||||
}
|
||||
chunkSize, parseErr := strconv.ParseUint(chunkSizeText, 16, 63)
|
||||
if parseErr != nil {
|
||||
return nil, nil, &protocolError{status: http.StatusBadRequest, message: "invalid chunk size"}
|
||||
}
|
||||
if chunkSize == 0 {
|
||||
trailer := make(http.Header)
|
||||
for {
|
||||
line, err = readLine()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if line == "" {
|
||||
return body.Bytes(), trailer, nil
|
||||
}
|
||||
if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
|
||||
return nil, nil, &protocolError{status: http.StatusBadRequest, message: "folded trailers are not supported"}
|
||||
}
|
||||
index := strings.IndexByte(line, ':')
|
||||
if index <= 0 {
|
||||
return nil, nil, &protocolError{status: http.StatusBadRequest, message: "malformed trailer"}
|
||||
}
|
||||
name := strings.TrimSpace(line[:index])
|
||||
value := strings.TrimSpace(line[index+1:])
|
||||
if !validToken(name) || !validHeaderValue(value) {
|
||||
return nil, nil, &protocolError{status: http.StatusBadRequest, message: "malformed trailer"}
|
||||
}
|
||||
trailer.Add(name, value)
|
||||
}
|
||||
}
|
||||
if int64(body.Len()) > maxBodyBytes-int64(chunkSize) {
|
||||
return nil, nil, &protocolError{status: http.StatusRequestEntityTooLarge, message: "request body too large"}
|
||||
}
|
||||
if setBodyDeadline != nil {
|
||||
setBodyDeadline()
|
||||
}
|
||||
if _, err := io.CopyN(&body, reader, int64(chunkSize)); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if setBodyDeadline != nil {
|
||||
setBodyDeadline()
|
||||
}
|
||||
var suffix [2]byte
|
||||
if _, err := io.ReadFull(reader, suffix[:]); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if suffix[0] != '\r' || suffix[1] != '\n' {
|
||||
return nil, nil, &protocolError{status: http.StatusBadRequest, message: "malformed chunk terminator"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validHexNumber(value string) bool {
|
||||
for index := 0; index < len(value); index++ {
|
||||
char := value[index]
|
||||
if !((char >= '0' && char <= '9') || (char >= 'a' && char <= 'f') || (char >= 'A' && char <= 'F')) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func parseProtocol(value string) (int, int, bool) {
|
||||
if value != "HTTP/1.1" && value != "HTTP/1.0" {
|
||||
return 0, 0, false
|
||||
|
||||
Reference in New Issue
Block a user