123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467 |
- // Copyright 2025 The Go Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style
- // license that can be found in the LICENSE file.
- package httpcommon
- import (
- "context"
- "errors"
- "fmt"
- "net/http/httptrace"
- "net/textproto"
- "net/url"
- "sort"
- "strconv"
- "strings"
- "golang.org/x/net/http/httpguts"
- "golang.org/x/net/http2/hpack"
- )
- var (
- ErrRequestHeaderListSize = errors.New("request header list larger than peer's advertised limit")
- )
- // Request is a subset of http.Request.
- // It'd be simpler to pass an *http.Request, of course, but we can't depend on net/http
- // without creating a dependency cycle.
- type Request struct {
- URL *url.URL
- Method string
- Host string
- Header map[string][]string
- Trailer map[string][]string
- ActualContentLength int64 // 0 means 0, -1 means unknown
- }
- // EncodeHeadersParam is parameters to EncodeHeaders.
- type EncodeHeadersParam struct {
- Request Request
- // AddGzipHeader indicates that an "accept-encoding: gzip" header should be
- // added to the request.
- AddGzipHeader bool
- // PeerMaxHeaderListSize, when non-zero, is the peer's MAX_HEADER_LIST_SIZE setting.
- PeerMaxHeaderListSize uint64
- // DefaultUserAgent is the User-Agent header to send when the request
- // neither contains a User-Agent nor disables it.
- DefaultUserAgent string
- }
- // EncodeHeadersParam is the result of EncodeHeaders.
- type EncodeHeadersResult struct {
- HasBody bool
- HasTrailers bool
- }
- // EncodeHeaders constructs request headers common to HTTP/2 and HTTP/3.
- // It validates a request and calls headerf with each pseudo-header and header
- // for the request.
- // The headerf function is called with the validated, canonicalized header name.
- func EncodeHeaders(ctx context.Context, param EncodeHeadersParam, headerf func(name, value string)) (res EncodeHeadersResult, _ error) {
- req := param.Request
- // Check for invalid connection-level headers.
- if err := checkConnHeaders(req.Header); err != nil {
- return res, err
- }
- if req.URL == nil {
- return res, errors.New("Request.URL is nil")
- }
- host := req.Host
- if host == "" {
- host = req.URL.Host
- }
- host, err := httpguts.PunycodeHostPort(host)
- if err != nil {
- return res, err
- }
- if !httpguts.ValidHostHeader(host) {
- return res, errors.New("invalid Host header")
- }
- // isNormalConnect is true if this is a non-extended CONNECT request.
- isNormalConnect := false
- var protocol string
- if vv := req.Header[":protocol"]; len(vv) > 0 {
- protocol = vv[0]
- }
- if req.Method == "CONNECT" && protocol == "" {
- isNormalConnect = true
- } else if protocol != "" && req.Method != "CONNECT" {
- return res, errors.New("invalid :protocol header in non-CONNECT request")
- }
- // Validate the path, except for non-extended CONNECT requests which have no path.
- var path string
- if !isNormalConnect {
- path = req.URL.RequestURI()
- if !validPseudoPath(path) {
- orig := path
- path = strings.TrimPrefix(path, req.URL.Scheme+"://"+host)
- if !validPseudoPath(path) {
- if req.URL.Opaque != "" {
- return res, fmt.Errorf("invalid request :path %q from URL.Opaque = %q", orig, req.URL.Opaque)
- } else {
- return res, fmt.Errorf("invalid request :path %q", orig)
- }
- }
- }
- }
- // Check for any invalid headers+trailers and return an error before we
- // potentially pollute our hpack state. (We want to be able to
- // continue to reuse the hpack encoder for future requests)
- if err := validateHeaders(req.Header); err != "" {
- return res, fmt.Errorf("invalid HTTP header %s", err)
- }
- if err := validateHeaders(req.Trailer); err != "" {
- return res, fmt.Errorf("invalid HTTP trailer %s", err)
- }
- trailers, err := commaSeparatedTrailers(req.Trailer)
- if err != nil {
- return res, err
- }
- enumerateHeaders := func(f func(name, value string)) {
- // 8.1.2.3 Request Pseudo-Header Fields
- // The :path pseudo-header field includes the path and query parts of the
- // target URI (the path-absolute production and optionally a '?' character
- // followed by the query production, see Sections 3.3 and 3.4 of
- // [RFC3986]).
- f(":authority", host)
- m := req.Method
- if m == "" {
- m = "GET"
- }
- f(":method", m)
- if !isNormalConnect {
- f(":path", path)
- f(":scheme", req.URL.Scheme)
- }
- if protocol != "" {
- f(":protocol", protocol)
- }
- if trailers != "" {
- f("trailer", trailers)
- }
- var didUA bool
- for k, vv := range req.Header {
- if asciiEqualFold(k, "host") || asciiEqualFold(k, "content-length") {
- // Host is :authority, already sent.
- // Content-Length is automatic, set below.
- continue
- } else if asciiEqualFold(k, "connection") ||
- asciiEqualFold(k, "proxy-connection") ||
- asciiEqualFold(k, "transfer-encoding") ||
- asciiEqualFold(k, "upgrade") ||
- asciiEqualFold(k, "keep-alive") {
- // Per 8.1.2.2 Connection-Specific Header
- // Fields, don't send connection-specific
- // fields. We have already checked if any
- // are error-worthy so just ignore the rest.
- continue
- } else if asciiEqualFold(k, "user-agent") {
- // Match Go's http1 behavior: at most one
- // User-Agent. If set to nil or empty string,
- // then omit it. Otherwise if not mentioned,
- // include the default (below).
- didUA = true
- if len(vv) < 1 {
- continue
- }
- vv = vv[:1]
- if vv[0] == "" {
- continue
- }
- } else if asciiEqualFold(k, "cookie") {
- // Per 8.1.2.5 To allow for better compression efficiency, the
- // Cookie header field MAY be split into separate header fields,
- // each with one or more cookie-pairs.
- for _, v := range vv {
- for {
- p := strings.IndexByte(v, ';')
- if p < 0 {
- break
- }
- f("cookie", v[:p])
- p++
- // strip space after semicolon if any.
- for p+1 <= len(v) && v[p] == ' ' {
- p++
- }
- v = v[p:]
- }
- if len(v) > 0 {
- f("cookie", v)
- }
- }
- continue
- } else if k == ":protocol" {
- // :protocol pseudo-header was already sent above.
- continue
- }
- for _, v := range vv {
- f(k, v)
- }
- }
- if shouldSendReqContentLength(req.Method, req.ActualContentLength) {
- f("content-length", strconv.FormatInt(req.ActualContentLength, 10))
- }
- if param.AddGzipHeader {
- f("accept-encoding", "gzip")
- }
- if !didUA {
- f("user-agent", param.DefaultUserAgent)
- }
- }
- // Do a first pass over the headers counting bytes to ensure
- // we don't exceed cc.peerMaxHeaderListSize. This is done as a
- // separate pass before encoding the headers to prevent
- // modifying the hpack state.
- if param.PeerMaxHeaderListSize > 0 {
- hlSize := uint64(0)
- enumerateHeaders(func(name, value string) {
- hf := hpack.HeaderField{Name: name, Value: value}
- hlSize += uint64(hf.Size())
- })
- if hlSize > param.PeerMaxHeaderListSize {
- return res, ErrRequestHeaderListSize
- }
- }
- trace := httptrace.ContextClientTrace(ctx)
- // Header list size is ok. Write the headers.
- enumerateHeaders(func(name, value string) {
- name, ascii := LowerHeader(name)
- if !ascii {
- // Skip writing invalid headers. Per RFC 7540, Section 8.1.2, header
- // field names have to be ASCII characters (just as in HTTP/1.x).
- return
- }
- headerf(name, value)
- if trace != nil && trace.WroteHeaderField != nil {
- trace.WroteHeaderField(name, []string{value})
- }
- })
- res.HasBody = req.ActualContentLength != 0
- res.HasTrailers = trailers != ""
- return res, nil
- }
- // IsRequestGzip reports whether we should add an Accept-Encoding: gzip header
- // for a request.
- func IsRequestGzip(method string, header map[string][]string, disableCompression bool) bool {
- // TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere?
- if !disableCompression &&
- len(header["Accept-Encoding"]) == 0 &&
- len(header["Range"]) == 0 &&
- method != "HEAD" {
- // Request gzip only, not deflate. Deflate is ambiguous and
- // not as universally supported anyway.
- // See: https://zlib.net/zlib_faq.html#faq39
- //
- // Note that we don't request this for HEAD requests,
- // due to a bug in nginx:
- // http://trac.nginx.org/nginx/ticket/358
- // https://golang.org/issue/5522
- //
- // We don't request gzip if the request is for a range, since
- // auto-decoding a portion of a gzipped document will just fail
- // anyway. See https://golang.org/issue/8923
- return true
- }
- return false
- }
- // checkConnHeaders checks whether req has any invalid connection-level headers.
- //
- // https://www.rfc-editor.org/rfc/rfc9114.html#section-4.2-3
- // https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.2-1
- //
- // Certain headers are special-cased as okay but not transmitted later.
- // For example, we allow "Transfer-Encoding: chunked", but drop the header when encoding.
- func checkConnHeaders(h map[string][]string) error {
- if vv := h["Upgrade"]; len(vv) > 0 && (vv[0] != "" && vv[0] != "chunked") {
- return fmt.Errorf("invalid Upgrade request header: %q", vv)
- }
- if vv := h["Transfer-Encoding"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && vv[0] != "chunked") {
- return fmt.Errorf("invalid Transfer-Encoding request header: %q", vv)
- }
- if vv := h["Connection"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && !asciiEqualFold(vv[0], "close") && !asciiEqualFold(vv[0], "keep-alive")) {
- return fmt.Errorf("invalid Connection request header: %q", vv)
- }
- return nil
- }
- func commaSeparatedTrailers(trailer map[string][]string) (string, error) {
- keys := make([]string, 0, len(trailer))
- for k := range trailer {
- k = CanonicalHeader(k)
- switch k {
- case "Transfer-Encoding", "Trailer", "Content-Length":
- return "", fmt.Errorf("invalid Trailer key %q", k)
- }
- keys = append(keys, k)
- }
- if len(keys) > 0 {
- sort.Strings(keys)
- return strings.Join(keys, ","), nil
- }
- return "", nil
- }
- // validPseudoPath reports whether v is a valid :path pseudo-header
- // value. It must be either:
- //
- // - a non-empty string starting with '/'
- // - the string '*', for OPTIONS requests.
- //
- // For now this is only used a quick check for deciding when to clean
- // up Opaque URLs before sending requests from the Transport.
- // See golang.org/issue/16847
- //
- // We used to enforce that the path also didn't start with "//", but
- // Google's GFE accepts such paths and Chrome sends them, so ignore
- // that part of the spec. See golang.org/issue/19103.
- func validPseudoPath(v string) bool {
- return (len(v) > 0 && v[0] == '/') || v == "*"
- }
- func validateHeaders(hdrs map[string][]string) string {
- for k, vv := range hdrs {
- if !httpguts.ValidHeaderFieldName(k) && k != ":protocol" {
- return fmt.Sprintf("name %q", k)
- }
- for _, v := range vv {
- if !httpguts.ValidHeaderFieldValue(v) {
- // Don't include the value in the error,
- // because it may be sensitive.
- return fmt.Sprintf("value for header %q", k)
- }
- }
- }
- return ""
- }
- // shouldSendReqContentLength reports whether we should send
- // a "content-length" request header. This logic is basically a copy of the net/http
- // transferWriter.shouldSendContentLength.
- // The contentLength is the corrected contentLength (so 0 means actually 0, not unknown).
- // -1 means unknown.
- func shouldSendReqContentLength(method string, contentLength int64) bool {
- if contentLength > 0 {
- return true
- }
- if contentLength < 0 {
- return false
- }
- // For zero bodies, whether we send a content-length depends on the method.
- // It also kinda doesn't matter for http2 either way, with END_STREAM.
- switch method {
- case "POST", "PUT", "PATCH":
- return true
- default:
- return false
- }
- }
- // ServerRequestParam is parameters to NewServerRequest.
- type ServerRequestParam struct {
- Method string
- Scheme, Authority, Path string
- Protocol string
- Header map[string][]string
- }
- // ServerRequestResult is the result of NewServerRequest.
- type ServerRequestResult struct {
- // Various http.Request fields.
- URL *url.URL
- RequestURI string
- Trailer map[string][]string
- NeedsContinue bool // client provided an "Expect: 100-continue" header
- // If the request should be rejected, this is a short string suitable for passing
- // to the http2 package's CountError function.
- // It might be a bit odd to return errors this way rather than returing an error,
- // but this ensures we don't forget to include a CountError reason.
- InvalidReason string
- }
- func NewServerRequest(rp ServerRequestParam) ServerRequestResult {
- needsContinue := httpguts.HeaderValuesContainsToken(rp.Header["Expect"], "100-continue")
- if needsContinue {
- delete(rp.Header, "Expect")
- }
- // Merge Cookie headers into one "; "-delimited value.
- if cookies := rp.Header["Cookie"]; len(cookies) > 1 {
- rp.Header["Cookie"] = []string{strings.Join(cookies, "; ")}
- }
- // Setup Trailers
- var trailer map[string][]string
- for _, v := range rp.Header["Trailer"] {
- for _, key := range strings.Split(v, ",") {
- key = textproto.CanonicalMIMEHeaderKey(textproto.TrimString(key))
- switch key {
- case "Transfer-Encoding", "Trailer", "Content-Length":
- // Bogus. (copy of http1 rules)
- // Ignore.
- default:
- if trailer == nil {
- trailer = make(map[string][]string)
- }
- trailer[key] = nil
- }
- }
- }
- delete(rp.Header, "Trailer")
- // "':authority' MUST NOT include the deprecated userinfo subcomponent
- // for "http" or "https" schemed URIs."
- // https://www.rfc-editor.org/rfc/rfc9113.html#section-8.3.1-2.3.8
- if strings.IndexByte(rp.Authority, '@') != -1 && (rp.Scheme == "http" || rp.Scheme == "https") {
- return ServerRequestResult{
- InvalidReason: "userinfo_in_authority",
- }
- }
- var url_ *url.URL
- var requestURI string
- if rp.Method == "CONNECT" && rp.Protocol == "" {
- url_ = &url.URL{Host: rp.Authority}
- requestURI = rp.Authority // mimic HTTP/1 server behavior
- } else {
- var err error
- url_, err = url.ParseRequestURI(rp.Path)
- if err != nil {
- return ServerRequestResult{
- InvalidReason: "bad_path",
- }
- }
- requestURI = rp.Path
- }
- return ServerRequestResult{
- URL: url_,
- NeedsContinue: needsContinue,
- RequestURI: requestURI,
- Trailer: trailer,
- }
- }
|