338 lines · 9.6 KB · Go
RawDownload
// Package httpwire contains narrowly scoped HTTP/1.1 wire helpers.package httpwire import (	"bytes"	"fmt"	"io"	"net"	"strconv"	"strings"	"sync") const maxBufferedRequestHeader = 1 << 20 // RequestHeaderOrder returns the desired header-name order for one HTTP/1.1// request. Names are compared case-insensitively. Headers omitted from the// returned list retain their original relative order after the listed headers.type RequestHeaderOrder func(method, requestTarget string) []string // NewOrderedRequestConn wraps conn and rewrites HTTP/1.1 request-header order// and matches header casing to the desired order list. Request lines, unlisted// header casing and values, and body bytes remain intact.func NewOrderedRequestConn(conn net.Conn, order RequestHeaderOrder) net.Conn {	if conn == nil || order == nil {		return conn	}	return &orderedRequestConn{Conn: conn, order: order}} type orderedRequestConn struct {	net.Conn	order RequestHeaderOrder 	mu            sync.Mutex	header        []byte	bodyRemaining int64	chunked       *chunkedRequestTracker} func (c *orderedRequestConn) Write(p []byte) (int, error) {	c.mu.Lock()	defer c.mu.Unlock() 	originalLength := len(p)	consumed := 0	remaining := p	for len(remaining) > 0 {		if c.bodyRemaining > 0 {			bodyBytes := min(int64(len(remaining)), c.bodyRemaining)			written, errWrite := writeAll(c.Conn, remaining[:bodyBytes])			consumed += written			c.bodyRemaining -= int64(written)			if errWrite != nil {				return consumed, errWrite			}			remaining = remaining[bodyBytes:]			continue		}		if c.chunked != nil {			preview := c.chunked.clone()			chunkBytes, _, errChunk := preview.consume(remaining)			if errChunk != nil {				return consumed, errChunk			}			written, errWrite := writeAll(c.Conn, remaining[:chunkBytes])			consumed += written			_, completed, errConsume := c.chunked.consume(remaining[:written])			if errConsume != nil {				return consumed, errConsume			}			if completed {				c.chunked = nil			}			if errWrite != nil {				return consumed, errWrite			}			remaining = remaining[chunkBytes:]			continue		} 		if len(c.header) == 0 && c.bodyRemaining == 0 && c.chunked == nil && !isHTTPMethodPrefix(remaining) {			written, errWrite := writeAll(c.Conn, remaining)			consumed += written			if errWrite != nil {				return consumed, errWrite			}			return originalLength, nil		} 		previousHeaderLength := len(c.header)		c.header = append(c.header, remaining...)		headerEnd := bytes.Index(c.header, []byte("\r\n\r\n"))		if headerEnd < 0 {			if len(c.header) > maxBufferedRequestHeader {				return consumed, fmt.Errorf("httpwire: request header exceeds %d bytes", maxBufferedRequestHeader)			}			return originalLength, nil		} 		headerEnd += len("\r\n\r\n")		header := c.header[:headerEnd]		body := c.header[headerEnd:]		c.header = nil		currentHeaderBytes := min(len(remaining), max(0, headerEnd-previousHeaderLength)) 		ordered, contentLength, chunked := orderRequestHeader(header, c.order)		if _, errWrite := writeAll(c.Conn, ordered); errWrite != nil {			// All caller bytes were accepted into the wrapper before the transformed			// header write failed. Return the full input count with the terminal			// connection error so callers do not replay an ambiguous partial header.			return originalLength, errWrite		}		consumed += currentHeaderBytes		remaining = body		if chunked {			c.chunked = newChunkedRequestTracker()			continue		}		c.bodyRemaining = contentLength	}	return originalLength, nil} func orderRequestHeader(header []byte, order RequestHeaderOrder) ([]byte, int64, bool) {	lines := bytes.Split(header[:len(header)-len("\r\n\r\n")], []byte("\r\n"))	if len(lines) == 0 {		return header, 0, false	}	requestParts := strings.SplitN(string(lines[0]), " ", 3)	if len(requestParts) != 3 {		return header, requestContentLength(lines[1:]), requestUsesChunkedEncoding(lines[1:])	} 	desired := order(requestParts[0], requestParts[1])	if len(desired) == 0 {		return header, requestContentLength(lines[1:]), requestUsesChunkedEncoding(lines[1:])	} 	headerLines := lines[1:]	used := make([]bool, len(headerLines))	orderedLines := make([][]byte, 0, len(lines))	orderedLines = append(orderedLines, lines[0])	for _, name := range desired {		for index, line := range headerLines {			if used[index] || !headerLineNamed(line, name) {				continue			}			colon := bytes.IndexByte(line, ':')			if colon > 0 && name != "" {				rewritten := make([]byte, 0, len(name)+len(line)-colon)				rewritten = append(rewritten, name...)				rewritten = append(rewritten, line[colon:]...)				orderedLines = append(orderedLines, rewritten)			} else {				orderedLines = append(orderedLines, line)			}			used[index] = true		}	}	for index, line := range headerLines {		if !used[index] {			orderedLines = append(orderedLines, line)		}	} 	var output bytes.Buffer	for _, line := range orderedLines {		output.Write(line)		output.WriteString("\r\n")	}	output.WriteString("\r\n")	return output.Bytes(), requestContentLength(headerLines), requestUsesChunkedEncoding(headerLines)} func headerLineNamed(line []byte, name string) bool {	colon := bytes.IndexByte(line, ':')	return colon > 0 && strings.EqualFold(string(line[:colon]), name)} func isHTTPTokenChar(b byte) bool {	return (b >= 'a' && b <= 'z') ||		(b >= 'A' && b <= 'Z') ||		(b >= '0' && b <= '9') ||		b == '!' || b == '#' || b == '$' || b == '%' || b == '&' ||		b == '\'' || b == '*' || b == '+' || b == '-' || b == '.' ||		b == '^' || b == '_' || b == '`' || b == '|' || b == '~'} func isHTTPMethodPrefix(p []byte) bool {	if len(p) == 0 {		return false	}	for i, b := range p {		if b == ' ' && i > 0 {			return true		}		if !isHTTPTokenChar(b) {			return false		}	}	return true} func requestContentLength(lines [][]byte) int64 {	for _, line := range lines {		if !headerLineNamed(line, "Content-Length") {			continue		}		colon := bytes.IndexByte(line, ':')		value := strings.TrimSpace(string(line[colon+1:]))		length, errParse := strconv.ParseInt(value, 10, 64)		if errParse == nil && length > 0 {			return length		}		return 0	}	return 0} func requestUsesChunkedEncoding(lines [][]byte) bool {	for _, line := range lines {		if !headerLineNamed(line, "Transfer-Encoding") {			continue		}		colon := bytes.IndexByte(line, ':')		for _, encoding := range strings.Split(string(line[colon+1:]), ",") {			if strings.EqualFold(strings.TrimSpace(encoding), "chunked") {				return true			}		}	}	return false} type chunkedRequestTracker struct {	state         uint8	line          []byte	dataRemaining int64	crlfPosition  int	trailers      []byte} const (	chunkedReadingSize uint8 = iota	chunkedReadingData	chunkedReadingDataCRLF	chunkedReadingTrailers) func newChunkedRequestTracker() *chunkedRequestTracker {	return &chunkedRequestTracker{state: chunkedReadingSize}} func (tracker *chunkedRequestTracker) clone() *chunkedRequestTracker {	cloned := *tracker	cloned.line = append([]byte(nil), tracker.line...)	cloned.trailers = append([]byte(nil), tracker.trailers...)	return &cloned} func (tracker *chunkedRequestTracker) consume(data []byte) (consumed int, completed bool, err error) {	for consumed < len(data) {		switch tracker.state {		case chunkedReadingSize:			tracker.line = append(tracker.line, data[consumed])			consumed++			if len(tracker.line) > maxBufferedRequestHeader {				return consumed, false, fmt.Errorf("httpwire: chunk size line exceeds %d bytes", maxBufferedRequestHeader)			}			if len(tracker.line) < 2 || !bytes.Equal(tracker.line[len(tracker.line)-2:], []byte("\r\n")) {				continue			}			sizeText := strings.TrimSpace(string(tracker.line[:len(tracker.line)-2]))			if extension := strings.IndexByte(sizeText, ';'); extension >= 0 {				sizeText = strings.TrimSpace(sizeText[:extension])			}			size, errParse := strconv.ParseInt(sizeText, 16, 64)			if errParse != nil || size < 0 {				return consumed, false, fmt.Errorf("httpwire: invalid chunk size %q", sizeText)			}			tracker.line = tracker.line[:0]			if size == 0 {				tracker.state = chunkedReadingTrailers				continue			}			tracker.dataRemaining = size			tracker.state = chunkedReadingData		case chunkedReadingData:			chunkBytes := min(int64(len(data)-consumed), tracker.dataRemaining)			consumed += int(chunkBytes)			tracker.dataRemaining -= chunkBytes			if tracker.dataRemaining == 0 {				tracker.crlfPosition = 0				tracker.state = chunkedReadingDataCRLF			}		case chunkedReadingDataCRLF:			want := []byte("\r\n")			if data[consumed] != want[tracker.crlfPosition] {				return consumed, false, fmt.Errorf("httpwire: chunk data is missing CRLF terminator")			}			consumed++			tracker.crlfPosition++			if tracker.crlfPosition == len(want) {				tracker.state = chunkedReadingSize			}		case chunkedReadingTrailers:			tracker.trailers = append(tracker.trailers, data[consumed])			consumed++			if len(tracker.trailers) > maxBufferedRequestHeader {				return consumed, false, fmt.Errorf("httpwire: chunk trailers exceed %d bytes", maxBufferedRequestHeader)			}			if bytes.Equal(tracker.trailers, []byte("\r\n")) ||				(len(tracker.trailers) >= 4 && bytes.Equal(tracker.trailers[len(tracker.trailers)-4:], []byte("\r\n\r\n"))) {				return consumed, true, nil			}		default:			return consumed, false, fmt.Errorf("httpwire: invalid chunk parser state %d", tracker.state)		}	}	return consumed, false, nil} func writeAll(writer io.Writer, data []byte) (int, error) {	total := 0	for len(data) > 0 {		written, errWrite := writer.Write(data)		total += written		if errWrite != nil {			return total, errWrite		}		if written <= 0 {			return total, io.ErrShortWrite		}		data = data[written:]	}	return total, nil}