Files
phishingclub/backend/vendor/github.com/enetx/http3/headers.go
T
RonniSkansing 2028391c3c vendor backend
Signed-off-by: RonniSkansing <rskansing@gmail.com>
2026-09-16 23:17:35 +02:00

430 lines
13 KiB
Go

package http3
import (
"bytes"
"errors"
"fmt"
"io"
"github.com/enetx/http"
"net/textproto"
"net/url"
"slices"
"strconv"
"strings"
"golang.org/x/net/http/httpguts"
"github.com/quic-go/qpack"
"github.com/quic-go/quic-go"
"github.com/enetx/http3/qlog"
"github.com/quic-go/quic-go/qlogwriter"
)
type qpackError struct{ err error }
func (e *qpackError) Error() string { return fmt.Sprintf("qpack: %v", e.err) }
func (e *qpackError) Unwrap() error { return e.err }
var errHeaderTooLarge = errors.New("http3: headers too large")
type header struct {
// Pseudo header fields defined in RFC 9114
Path string
Method string
Authority string
Scheme string
Status string
// for Extended connect
Protocol string
// parsed and deduplicated. -1 if no Content-Length header is sent
ContentLength int64
// all non-pseudo headers
Headers http.Header
}
// connection-specific header fields must not be sent on HTTP/3
var invalidHeaderFields = [...]string{
"connection",
"keep-alive",
"proxy-connection",
"transfer-encoding",
"upgrade",
}
func parseHeaders(decodeFn qpack.DecodeFunc, isRequest bool, sizeLimit int, headerFields *[]qpack.HeaderField) (header, error) {
hdr := header{Headers: make(http.Header)}
var readFirstRegularHeader, readContentLength bool
var contentLengthStr string
for {
h, err := decodeFn()
if err != nil {
if err == io.EOF {
break
}
return header{}, &qpackError{err}
}
if headerFields != nil {
*headerFields = append(*headerFields, h)
}
// RFC 9114, section 4.2.2:
// The size of a field list is calculated based on the uncompressed size of fields,
// including the length of the name and value in bytes plus an overhead of 32 bytes for each field.
sizeLimit -= len(h.Name) + len(h.Value) + 32
if sizeLimit < 0 {
return header{}, errHeaderTooLarge
}
if err := validateHeaderFieldNameAndValue(h); err != nil {
return header{}, err
}
if h.IsPseudo() {
if readFirstRegularHeader {
// all pseudo headers must appear before regular header fields, see section 4.3 of RFC 9114
return header{}, fmt.Errorf("received pseudo header %s after a regular header field", h.Name)
}
var isResponsePseudoHeader bool // pseudo headers are either valid for requests or for responses
var isDuplicatePseudoHeader bool // pseudo headers are allowed to appear exactly once
switch h.Name {
case ":path":
isDuplicatePseudoHeader = hdr.Path != ""
hdr.Path = h.Value
case ":method":
isDuplicatePseudoHeader = hdr.Method != ""
hdr.Method = h.Value
case ":authority":
isDuplicatePseudoHeader = hdr.Authority != ""
hdr.Authority = h.Value
case ":protocol": // RFC 9220
isDuplicatePseudoHeader = hdr.Protocol != ""
hdr.Protocol = h.Value
case ":scheme":
isDuplicatePseudoHeader = hdr.Scheme != ""
hdr.Scheme = h.Value
case ":status":
isDuplicatePseudoHeader = hdr.Status != ""
hdr.Status = h.Value
isResponsePseudoHeader = true
default:
return header{}, fmt.Errorf("unknown pseudo header: %s", h.Name)
}
if isDuplicatePseudoHeader {
return header{}, fmt.Errorf("duplicate pseudo header: %s", h.Name)
}
if isRequest && isResponsePseudoHeader {
return header{}, fmt.Errorf("invalid request pseudo header: %s", h.Name)
}
if !isRequest && !isResponsePseudoHeader {
return header{}, fmt.Errorf("invalid response pseudo header: %s", h.Name)
}
} else {
if err := validateRegularHeaderField(h); err != nil {
return header{}, err
}
readFirstRegularHeader = true
switch h.Name {
case "content-length":
// Ignore duplicate Content-Length headers.
// Fail if the duplicates differ.
if !readContentLength {
readContentLength = true
contentLengthStr = h.Value
} else if contentLengthStr != h.Value {
return header{}, fmt.Errorf("contradicting content lengths (%s and %s)", contentLengthStr, h.Value)
}
default:
hdr.Headers.Add(h.Name, h.Value)
}
}
}
hdr.ContentLength = -1
if len(contentLengthStr) > 0 {
// use ParseUint instead of ParseInt, so that parsing fails on negative values
cl, err := strconv.ParseUint(contentLengthStr, 10, 63)
if err != nil {
return header{}, fmt.Errorf("invalid content length: %w", err)
}
hdr.Headers.Set("Content-Length", contentLengthStr)
hdr.ContentLength = int64(cl)
}
return hdr, nil
}
func validateHeaderFieldNameAndValue(h qpack.HeaderField) error {
// field names need to be lowercase, see section 4.2 of RFC 9114
if strings.ToLower(h.Name) != h.Name {
return fmt.Errorf("header field is not lower-case: %s", h.Name)
}
if !httpguts.ValidHeaderFieldValue(h.Value) {
return fmt.Errorf("invalid header field value for %s: %q", h.Name, h.Value)
}
return nil
}
func validateRegularHeaderField(h qpack.HeaderField) error {
if !httpguts.ValidHeaderFieldName(h.Name) {
return fmt.Errorf("invalid header field name: %q", h.Name)
}
if slices.Contains(invalidHeaderFields[:], h.Name) {
return fmt.Errorf("invalid header field name: %q", h.Name)
}
if h.Name == "te" && h.Value != "trailers" {
return fmt.Errorf("invalid TE header field value: %q", h.Value)
}
return nil
}
func validateTrailerHeaderField(h qpack.HeaderField) error {
if err := validateRegularHeaderField(h); err != nil {
return err
}
if !httpguts.ValidTrailerHeader(h.Name) {
return fmt.Errorf("invalid trailer field name: %q", h.Name)
}
return nil
}
func parseTrailers(decodeFn qpack.DecodeFunc, sizeLimit int, headerFields *[]qpack.HeaderField) (http.Header, error) {
h := make(http.Header)
for {
hf, err := decodeFn()
if err != nil {
if err == io.EOF {
break
}
return nil, &qpackError{err}
}
if headerFields != nil {
*headerFields = append(*headerFields, hf)
}
// RFC 9114, section 4.2.2:
// The size of a field list is calculated based on the uncompressed size of fields,
// including the length of the name and value in bytes plus an overhead of 32 bytes for each field.
sizeLimit -= len(hf.Name) + len(hf.Value) + 32
if sizeLimit < 0 {
return nil, errHeaderTooLarge
}
if err := validateHeaderFieldNameAndValue(hf); err != nil {
return nil, err
}
if hf.IsPseudo() {
return nil, fmt.Errorf("http3: received pseudo header in trailer: %s", hf.Name)
}
if err := validateTrailerHeaderField(hf); err != nil {
return nil, err
}
h.Add(hf.Name, hf.Value)
}
return h, nil
}
func requestFromHeaders(decodeFn qpack.DecodeFunc, sizeLimit int, headerFields *[]qpack.HeaderField) (*http.Request, error) {
hdr, err := parseHeaders(decodeFn, true, sizeLimit, headerFields)
if err != nil {
return nil, err
}
// concatenate cookie headers, see https://tools.ietf.org/html/rfc6265#section-5.4
if len(hdr.Headers["Cookie"]) > 0 {
hdr.Headers.Set("Cookie", strings.Join(hdr.Headers["Cookie"], "; "))
}
isConnect := hdr.Method == http.MethodConnect
// Extended CONNECT, see https://datatracker.ietf.org/doc/html/rfc8441#section-4
isExtendedConnected := isConnect && hdr.Protocol != ""
if isExtendedConnected {
if !validExtendedConnectProtocol(hdr.Protocol) {
return nil, fmt.Errorf("invalid :protocol: %q", hdr.Protocol)
}
if hdr.Scheme == "" || hdr.Path == "" || hdr.Authority == "" {
return nil, errors.New("extended CONNECT: :scheme, :path and :authority must not be empty")
}
} else if isConnect {
if hdr.Path != "" || hdr.Authority == "" { // normal CONNECT
return nil, errors.New(":path must be empty and :authority must not be empty")
}
} else if len(hdr.Path) == 0 || len(hdr.Authority) == 0 || len(hdr.Method) == 0 {
return nil, errors.New(":path, :authority and :method must not be empty")
}
if !isExtendedConnected && len(hdr.Protocol) > 0 {
return nil, errors.New(":protocol must be empty")
}
var u *url.URL
var requestURI string
protocol := "HTTP/3.0"
if isConnect {
u = &url.URL{}
if isExtendedConnected {
u, err = url.ParseRequestURI(hdr.Path)
if err != nil {
return nil, err
}
protocol = hdr.Protocol
} else {
u.Path = hdr.Path
}
requestURI = hdr.Authority
} else {
u, err = url.ParseRequestURI(hdr.Path)
if err != nil {
return nil, fmt.Errorf("invalid request URI: %w", err)
}
requestURI = hdr.Path
}
u.Scheme = hdr.Scheme
u.Host = hdr.Authority
req := &http.Request{
Method: hdr.Method,
URL: u,
Proto: protocol,
ProtoMajor: 3,
ProtoMinor: 0,
Header: hdr.Headers,
Body: nil,
ContentLength: hdr.ContentLength,
Host: hdr.Authority,
RequestURI: requestURI,
}
req.Trailer = extractAnnouncedTrailers(req.Header)
return req, nil
}
func validExtendedConnectProtocol(protocol string) bool {
// RFC 9220 specifies that the semantics of the :protocol pseudo are the same as defined in RFC 8441.
// RFC 8441, Section 4 specifies that :protocol is a single value from the HTTP Upgrade Token Registry.
// RFC 9110, Section 16.7 specifies that HTTP Upgrade Token Registry uses token grammar.
// Therefore, ValidHeaderFieldName is the right syntax check here, despite the misleading name.
return httpguts.ValidHeaderFieldName(protocol)
}
// updateResponseFromHeaders sets up http.Response as an HTTP/3 response,
// using the decoded qpack header filed.
// It is only called for the HTTP header (and not the HTTP trailer).
// It takes an http.Response as an argument to allow the caller to set the trailer later on.
func updateResponseFromHeaders(rsp *http.Response, decodeFn qpack.DecodeFunc, sizeLimit int, headerFields *[]qpack.HeaderField) error {
hdr, err := parseHeaders(decodeFn, false, sizeLimit, headerFields)
if err != nil {
return err
}
if hdr.Status == "" {
return errors.New("missing :status field")
}
rsp.Proto = "HTTP/3.0"
rsp.ProtoMajor = 3
rsp.Header = hdr.Headers
rsp.Trailer = extractAnnouncedTrailers(rsp.Header)
rsp.ContentLength = hdr.ContentLength
status, err := strconv.Atoi(hdr.Status)
if err != nil {
return fmt.Errorf("invalid status code: %w", err)
}
rsp.StatusCode = status
rsp.Status = hdr.Status + " " + http.StatusText(status)
return nil
}
// extractAnnouncedTrailers extracts trailer keys from the "Trailer" header.
// It returns a map with the announced keys set to nil values, and removes the "Trailer" header.
// It handles both duplicate as well as comma-separated values for the Trailer header.
// For example:
//
// Trailer: Trailer1, Trailer2
// Trailer: Trailer3
//
// Will result in a map containing the keys "Trailer1", "Trailer2", "Trailer3" with nil values.
func extractAnnouncedTrailers(header http.Header) http.Header {
rawTrailers, ok := header["Trailer"]
if !ok {
return nil
}
trailers := make(http.Header)
for _, rawVal := range rawTrailers {
for val := range strings.SplitSeq(rawVal, ",") {
trailers[http.CanonicalHeaderKey(textproto.TrimString(val))] = nil
}
}
delete(header, "Trailer")
return trailers
}
// writeTrailers encodes and writes HTTP trailers as a HEADERS frame.
// It returns true if trailers were written, false if there were no trailers to write.
func writeTrailers(wr io.Writer, trailers http.Header, streamID quic.StreamID, qlogger qlogwriter.Recorder) (bool, error) {
var hasValues bool
for k, vals := range trailers {
if httpguts.ValidTrailerHeader(k) && len(vals) > 0 {
hasValues = true
break
}
}
if !hasValues {
return false, nil
}
var buf bytes.Buffer
enc := qpack.NewEncoder(&buf)
var headerFields []qlog.HeaderField
if qlogger != nil {
headerFields = make([]qlog.HeaderField, 0, len(trailers))
}
for k, vals := range trailers {
if len(vals) == 0 {
continue
}
if !httpguts.ValidTrailerHeader(k) {
continue
}
lowercaseKey := strings.ToLower(k)
for _, v := range vals {
if err := enc.WriteField(qpack.HeaderField{Name: lowercaseKey, Value: v}); err != nil {
return false, err
}
if qlogger != nil {
headerFields = append(headerFields, qlog.HeaderField{Name: lowercaseKey, Value: v})
}
}
}
b := make([]byte, 0, frameHeaderLen+buf.Len())
b = (&headersFrame{Length: uint64(buf.Len())}).Append(b)
b = append(b, buf.Bytes()...)
if qlogger != nil {
qlogCreatedHeadersFrame(qlogger, streamID, len(b), buf.Len(), headerFields)
}
_, err := wr.Write(b)
return true, err
}
func decodeTrailers(r io.Reader, hf *headersFrame, maxHeaderBytes int, decoder *qpack.Decoder, qlogger qlogwriter.Recorder, streamID quic.StreamID) (http.Header, error) {
if hf.Length > uint64(maxHeaderBytes) {
maybeQlogInvalidHeadersFrame(qlogger, streamID, hf.Length)
return nil, fmt.Errorf("http3: HEADERS frame too large: %d bytes (max: %d)", hf.Length, maxHeaderBytes)
}
b := make([]byte, hf.Length)
if _, err := io.ReadFull(r, b); err != nil {
return nil, err
}
decodeFn := decoder.Decode(b)
var fields []qpack.HeaderField
var headerFields *[]qpack.HeaderField
if qlogger != nil {
fields = make([]qpack.HeaderField, 0, 16)
headerFields = &fields
}
trailers, err := parseTrailers(decodeFn, maxHeaderBytes, headerFields)
if err != nil {
maybeQlogInvalidHeadersFrame(qlogger, streamID, hf.Length)
return nil, err
}
if qlogger != nil {
qlogParsedHeadersFrame(qlogger, streamID, hf, fields)
}
return trailers, nil
}