]> Cypherpunks repositories - gostls13.git/commitdiff
net/http, net/http/internal/httpcommon: add httpcommon package
authorDamien Neil <dneil@google.com>
Thu, 13 Feb 2025 22:38:09 +0000 (14:38 -0800)
committerGopher Robot <gobot@golang.org>
Fri, 14 Feb 2025 00:29:58 +0000 (16:29 -0800)
The golang.org/x/net/internal/httpcommon package is
a new package containing internal functions common to the
HTTP/2 and HTTP/3 implementations.

Update to golang.org/x/net@v0.35.1-0.20250213222735-884432780bfd,
which includes the httpcommon package.

Since net/http can't depend on a x/net/internal package,
add net/http/internal/httpcommon which bundles the x/net
package.

Change-Id: Iba6c4be7b3e2d9a9d79c4b5153497b0e04b4497b
Reviewed-on: https://go-review.googlesource.com/c/go/+/649296
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
src/go.mod
src/go.sum
src/go/build/deps_test.go
src/net/http/h2_bundle.go
src/net/http/http.go
src/net/http/http_test.go
src/net/http/internal/httpcommon/httpcommon.go [new file with mode: 0644]
src/vendor/modules.txt

index 4ccf4ff79eed2f0578ef8c70eeac3cd7bc00b77d..c0bbca7e29bcc4783b1d05abdcd81edc87a45cee 100644 (file)
@@ -4,7 +4,7 @@ go 1.25
 
 require (
        golang.org/x/crypto v0.33.1-0.20250210163342-e47973b1c108
-       golang.org/x/net v0.34.1-0.20250123000230-c72e89d6a9e4
+       golang.org/x/net v0.35.1-0.20250213222735-884432780bfd
 )
 
 require (
index 50dec70ed6e6f228d706d8d49173c985a9fadac6..61223c0bbb6ee0d43963e4d8d22ed82d619a63d0 100644 (file)
@@ -1,7 +1,7 @@
 golang.org/x/crypto v0.33.1-0.20250210163342-e47973b1c108 h1:FwaGHNRX5GDt6vHr+Ly+yRTs0ADe4xTlGOzwaga4ZOs=
 golang.org/x/crypto v0.33.1-0.20250210163342-e47973b1c108/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
-golang.org/x/net v0.34.1-0.20250123000230-c72e89d6a9e4 h1:guLo+MhruvDNVBe2ssFzu5BGn4pc0G1xx6TqTHK+MnE=
-golang.org/x/net v0.34.1-0.20250123000230-c72e89d6a9e4/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
+golang.org/x/net v0.35.1-0.20250213222735-884432780bfd h1:NtufTkm/X6BNpniJAbESf1Mvax5jGy+/oP53IEn5RiA=
+golang.org/x/net v0.35.1-0.20250213222735-884432780bfd/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
 golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
 golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
index 29773486dd81769d87eb7bb5884e0639aafb2b5a..580500c033e1fc098d9e48deaca26a6a22d48d32 100644 (file)
@@ -614,6 +614,7 @@ var depsRules = `
        net/http/httptrace,
        mime/multipart,
        log
+       < net/http/internal/httpcommon
        < net/http;
 
        # HTTP-aware packages
index 81a8c4bc4dedbfce6aa935b69ec50ce4c78a6d02..9933c413ac84d499556692329caa5d5ebb99082b 100644 (file)
@@ -1,7 +1,7 @@
 //go:build !nethttpomithttp2
 
 // Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.
-//   $ bundle -o=h2_bundle.go -prefix=http2 -tags=!nethttpomithttp2 golang.org/x/net/http2
+//   $ bundle -o=h2_bundle.go -prefix=http2 -tags=!nethttpomithttp2 -import=golang.org/x/net/internal/httpcommon=net/http/internal/httpcommon golang.org/x/net/http2
 
 // Package http2 implements the HTTP/2 protocol.
 //
@@ -40,6 +40,7 @@ import (
        mathrand "math/rand"
        "net"
        "net/http/httptrace"
+       "net/http/internal/httpcommon"
        "net/textproto"
        "net/url"
        "os"
@@ -3413,101 +3414,6 @@ func http2cutoff64(base int) uint64 {
        return (1<<64-1)/uint64(base) + 1
 }
 
-var (
-       http2commonBuildOnce   sync.Once
-       http2commonLowerHeader map[string]string // Go-Canonical-Case -> lower-case
-       http2commonCanonHeader map[string]string // lower-case -> Go-Canonical-Case
-)
-
-func http2buildCommonHeaderMapsOnce() {
-       http2commonBuildOnce.Do(http2buildCommonHeaderMaps)
-}
-
-func http2buildCommonHeaderMaps() {
-       common := []string{
-               "accept",
-               "accept-charset",
-               "accept-encoding",
-               "accept-language",
-               "accept-ranges",
-               "age",
-               "access-control-allow-credentials",
-               "access-control-allow-headers",
-               "access-control-allow-methods",
-               "access-control-allow-origin",
-               "access-control-expose-headers",
-               "access-control-max-age",
-               "access-control-request-headers",
-               "access-control-request-method",
-               "allow",
-               "authorization",
-               "cache-control",
-               "content-disposition",
-               "content-encoding",
-               "content-language",
-               "content-length",
-               "content-location",
-               "content-range",
-               "content-type",
-               "cookie",
-               "date",
-               "etag",
-               "expect",
-               "expires",
-               "from",
-               "host",
-               "if-match",
-               "if-modified-since",
-               "if-none-match",
-               "if-unmodified-since",
-               "last-modified",
-               "link",
-               "location",
-               "max-forwards",
-               "origin",
-               "proxy-authenticate",
-               "proxy-authorization",
-               "range",
-               "referer",
-               "refresh",
-               "retry-after",
-               "server",
-               "set-cookie",
-               "strict-transport-security",
-               "trailer",
-               "transfer-encoding",
-               "user-agent",
-               "vary",
-               "via",
-               "www-authenticate",
-               "x-forwarded-for",
-               "x-forwarded-proto",
-       }
-       http2commonLowerHeader = make(map[string]string, len(common))
-       http2commonCanonHeader = make(map[string]string, len(common))
-       for _, v := range common {
-               chk := CanonicalHeaderKey(v)
-               http2commonLowerHeader[chk] = v
-               http2commonCanonHeader[v] = chk
-       }
-}
-
-func http2lowerHeader(v string) (lower string, ascii bool) {
-       http2buildCommonHeaderMapsOnce()
-       if s, ok := http2commonLowerHeader[v]; ok {
-               return s, true
-       }
-       return http2asciiToLower(v)
-}
-
-func http2canonicalHeader(v string) string {
-       http2buildCommonHeaderMapsOnce()
-       if s, ok := http2commonCanonHeader[v]; ok {
-               return s
-       }
-       return CanonicalHeaderKey(v)
-}
-
 var (
        http2VerboseLogs    bool
        http2logFrameWrites bool
@@ -3894,23 +3800,6 @@ func (s *http2sorter) SortStrings(ss []string) {
        s.v = save
 }
 
-// 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 http2validPseudoPath(v string) bool {
-       return (len(v) > 0 && v[0] == '/') || v == "*"
-}
-
 // incomparable is a zero-width, non-comparable type. Adding it to a struct
 // makes that struct also non-comparable, and generally doesn't add
 // any size (as long as it's first).
@@ -4863,8 +4752,7 @@ const http2maxCachedCanonicalHeadersKeysSize = 2048
 
 func (sc *http2serverConn) canonicalHeader(v string) string {
        sc.serveG.check()
-       http2buildCommonHeaderMapsOnce()
-       cv, ok := http2commonCanonHeader[v]
+       cv, ok := httpcommon.CachedCanonicalHeader(v)
        if ok {
                return cv
        }
@@ -8698,23 +8586,6 @@ func (cc *http2ClientConn) closeForLostPing() {
 // exported. At least they'll be DeepEqual for h1-vs-h2 comparisons tests.
 var http2errRequestCanceled = errors.New("net/http: request canceled")
 
-func http2commaSeparatedTrailers(req *Request) (string, error) {
-       keys := make([]string, 0, len(req.Trailer))
-       for k := range req.Trailer {
-               k = http2canonicalHeader(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
-}
-
 func (cc *http2ClientConn) responseHeaderTimeout() time.Duration {
        if cc.t.t1 != nil {
                return cc.t.t1.ResponseHeaderTimeout
@@ -8726,22 +8597,6 @@ func (cc *http2ClientConn) responseHeaderTimeout() time.Duration {
        return 0
 }
 
-// checkConnHeaders checks whether req has any invalid connection-level headers.
-// per RFC 7540 section 8.1.2.2: Connection-Specific Header Fields.
-// Certain headers are special-cased as okay but not transmitted later.
-func http2checkConnHeaders(req *Request) error {
-       if v := req.Header.Get("Upgrade"); v != "" {
-               return fmt.Errorf("http2: invalid Upgrade request header: %q", req.Header["Upgrade"])
-       }
-       if vv := req.Header["Transfer-Encoding"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && vv[0] != "chunked") {
-               return fmt.Errorf("http2: invalid Transfer-Encoding request header: %q", vv)
-       }
-       if vv := req.Header["Connection"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && !http2asciiEqualFold(vv[0], "close") && !http2asciiEqualFold(vv[0], "keep-alive")) {
-               return fmt.Errorf("http2: invalid Connection request header: %q", vv)
-       }
-       return nil
-}
-
 // actualContentLength returns a sanitized version of
 // req.ContentLength, where 0 actually means zero (not unknown) and -1
 // means unknown.
@@ -8787,25 +8642,7 @@ func (cc *http2ClientConn) roundTrip(req *Request, streamf func(*http2clientStre
                donec:                make(chan struct{}),
        }
 
-       // TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere?
-       if !cc.t.disableCompression() &&
-               req.Header.Get("Accept-Encoding") == "" &&
-               req.Header.Get("Range") == "" &&
-               !cs.isHead {
-               // 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
-               cs.requestedGzip = true
-       }
+       cs.requestedGzip = httpcommon.IsRequestGzip(req.Method, req.Header, cc.t.disableCompression())
 
        go cs.doRequest(req, streamf)
 
@@ -8919,10 +8756,6 @@ func (cs *http2clientStream) writeRequest(req *Request, streamf func(*http2clien
        cc := cs.cc
        ctx := cs.ctx
 
-       if err := http2checkConnHeaders(req); err != nil {
-               return err
-       }
-
        // wait for setting frames to be received, a server can change this value later,
        // but we just wait for the first settings frame
        var isExtendedConnect bool
@@ -9086,26 +8919,39 @@ func (cs *http2clientStream) encodeAndWriteHeaders(req *Request) error {
        // we send: HEADERS{1}, CONTINUATION{0,} + DATA{0,} (DATA is
        // sent by writeRequestBody below, along with any Trailers,
        // again in form HEADERS{1}, CONTINUATION{0,})
-       trailers, err := http2commaSeparatedTrailers(req)
-       if err != nil {
-               return err
-       }
-       hasTrailers := trailers != ""
-       contentLen := http2actualContentLength(req)
-       hasBody := contentLen != 0
-       hdrs, err := cc.encodeHeaders(req, cs.requestedGzip, trailers, contentLen)
+       cc.hbuf.Reset()
+       res, err := http2encodeRequestHeaders(req, cs.requestedGzip, cc.peerMaxHeaderListSize, func(name, value string) {
+               cc.writeHeader(name, value)
+       })
        if err != nil {
-               return err
+               return fmt.Errorf("http2: %w", err)
        }
+       hdrs := cc.hbuf.Bytes()
 
        // Write the request.
-       endStream := !hasBody && !hasTrailers
+       endStream := !res.HasBody && !res.HasTrailers
        cs.sentHeaders = true
        err = cc.writeHeaders(cs.ID, endStream, int(cc.maxFrameSize), hdrs)
        http2traceWroteHeaders(cs.trace)
        return err
 }
 
+func http2encodeRequestHeaders(req *Request, addGzipHeader bool, peerMaxHeaderListSize uint64, headerf func(name, value string)) (httpcommon.EncodeHeadersResult, error) {
+       return httpcommon.EncodeHeaders(req.Context(), httpcommon.EncodeHeadersParam{
+               Request: httpcommon.Request{
+                       Header:              req.Header,
+                       Trailer:             req.Trailer,
+                       URL:                 req.URL,
+                       Host:                req.Host,
+                       Method:              req.Method,
+                       ActualContentLength: http2actualContentLength(req),
+               },
+               AddGzipHeader:         addGzipHeader,
+               PeerMaxHeaderListSize: peerMaxHeaderListSize,
+               DefaultUserAgent:      http2defaultUserAgent,
+       }, headerf)
+}
+
 // cleanupWriteRequest performs post-request tasks.
 //
 // If err (the result of writeRequest) is non-nil and the stream is not closed,
@@ -9494,229 +9340,6 @@ func (cs *http2clientStream) awaitFlowControl(maxBytes int) (taken int32, err er
        }
 }
 
-func http2validateHeaders(hdrs Header) 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 ""
-}
-
-var http2errNilRequestURL = errors.New("http2: Request.URI is nil")
-
-// requires cc.wmu be held.
-func (cc *http2ClientConn) encodeHeaders(req *Request, addGzipHeader bool, trailers string, contentLength int64) ([]byte, error) {
-       cc.hbuf.Reset()
-       if req.URL == nil {
-               return nil, http2errNilRequestURL
-       }
-
-       host := req.Host
-       if host == "" {
-               host = req.URL.Host
-       }
-       host, err := httpguts.PunycodeHostPort(host)
-       if err != nil {
-               return nil, err
-       }
-       if !httpguts.ValidHostHeader(host) {
-               return nil, errors.New("http2: invalid Host header")
-       }
-
-       // isNormalConnect is true if this is a non-extended CONNECT request.
-       isNormalConnect := false
-       protocol := req.Header.Get(":protocol")
-       if req.Method == "CONNECT" && protocol == "" {
-               isNormalConnect = true
-       } else if protocol != "" && req.Method != "CONNECT" {
-               return nil, errors.New("http2: invalid :protocol header in non-CONNECT request")
-       }
-
-       var path string
-       if !isNormalConnect {
-               path = req.URL.RequestURI()
-               if !http2validPseudoPath(path) {
-                       orig := path
-                       path = strings.TrimPrefix(path, req.URL.Scheme+"://"+host)
-                       if !http2validPseudoPath(path) {
-                               if req.URL.Opaque != "" {
-                                       return nil, fmt.Errorf("invalid request :path %q from URL.Opaque = %q", orig, req.URL.Opaque)
-                               } else {
-                                       return nil, 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 := http2validateHeaders(req.Header); err != "" {
-               return nil, fmt.Errorf("invalid HTTP header %s", err)
-       }
-       if err := http2validateHeaders(req.Trailer); err != "" {
-               return nil, fmt.Errorf("invalid HTTP trailer %s", 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 = MethodGet
-               }
-               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 http2asciiEqualFold(k, "host") || http2asciiEqualFold(k, "content-length") {
-                               // Host is :authority, already sent.
-                               // Content-Length is automatic, set below.
-                               continue
-                       } else if http2asciiEqualFold(k, "connection") ||
-                               http2asciiEqualFold(k, "proxy-connection") ||
-                               http2asciiEqualFold(k, "transfer-encoding") ||
-                               http2asciiEqualFold(k, "upgrade") ||
-                               http2asciiEqualFold(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 http2asciiEqualFold(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 http2asciiEqualFold(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 http2shouldSendReqContentLength(req.Method, contentLength) {
-                       f("content-length", strconv.FormatInt(contentLength, 10))
-               }
-               if addGzipHeader {
-                       f("accept-encoding", "gzip")
-               }
-               if !didUA {
-                       f("user-agent", http2defaultUserAgent)
-               }
-       }
-
-       // 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.
-       hlSize := uint64(0)
-       enumerateHeaders(func(name, value string) {
-               hf := hpack.HeaderField{Name: name, Value: value}
-               hlSize += uint64(hf.Size())
-       })
-
-       if hlSize > cc.peerMaxHeaderListSize {
-               return nil, http2errRequestHeaderListSize
-       }
-
-       trace := httptrace.ContextClientTrace(req.Context())
-       traceHeaders := http2traceHasWroteHeaderField(trace)
-
-       // Header list size is ok. Write the headers.
-       enumerateHeaders(func(name, value string) {
-               name, ascii := http2lowerHeader(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
-               }
-               cc.writeHeader(name, value)
-               if traceHeaders {
-                       http2traceWroteHeaderField(trace, name, value)
-               }
-       })
-
-       return cc.hbuf.Bytes(), nil
-}
-
-// shouldSendReqContentLength reports whether the http2.Transport 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 http2shouldSendReqContentLength(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
-       }
-}
-
 // requires cc.wmu be held.
 func (cc *http2ClientConn) encodeTrailers(trailer Header) ([]byte, error) {
        cc.hbuf.Reset()
@@ -9733,7 +9356,7 @@ func (cc *http2ClientConn) encodeTrailers(trailer Header) ([]byte, error) {
        }
 
        for k, vv := range trailer {
-               lowKey, ascii := http2lowerHeader(k)
+               lowKey, ascii := httpcommon.LowerHeader(k)
                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).
@@ -10088,7 +9711,7 @@ func (rl *http2clientConnReadLoop) handleResponse(cs *http2clientStream, f *http
                Status:     status + " " + StatusText(statusCode),
        }
        for _, hf := range regularFields {
-               key := http2canonicalHeader(hf.Name)
+               key := httpcommon.CanonicalHeader(hf.Name)
                if key == "Trailer" {
                        t := res.Trailer
                        if t == nil {
@@ -10096,7 +9719,7 @@ func (rl *http2clientConnReadLoop) handleResponse(cs *http2clientStream, f *http
                                res.Trailer = t
                        }
                        http2foreachHeaderElement(hf.Value, func(v string) {
-                               t[http2canonicalHeader(v)] = nil
+                               t[httpcommon.CanonicalHeader(v)] = nil
                        })
                } else {
                        vv := header[key]
@@ -10220,7 +9843,7 @@ func (rl *http2clientConnReadLoop) processTrailers(cs *http2clientStream, f *htt
 
        trailer := make(Header)
        for _, hf := range f.RegularFields() {
-               key := http2canonicalHeader(hf.Name)
+               key := httpcommon.CanonicalHeader(hf.Name)
                trailer[key] = append(trailer[key], hf.Value)
        }
        cs.trailer = trailer
@@ -10766,7 +10389,7 @@ func (cc *http2ClientConn) writeStreamReset(streamID uint32, code http2ErrCode,
 
 var (
        http2errResponseHeaderListSize = errors.New("http2: response header list larger than advertised limit")
-       http2errRequestHeaderListSize  = errors.New("http2: request header list larger than peer's advertised limit")
+       http2errRequestHeaderListSize  = httpcommon.ErrRequestHeaderListSize
 )
 
 func (cc *http2ClientConn) logf(format string, args ...interface{}) {
@@ -10953,16 +10576,6 @@ func http2traceFirstResponseByte(trace *httptrace.ClientTrace) {
        }
 }
 
-func http2traceHasWroteHeaderField(trace *httptrace.ClientTrace) bool {
-       return trace != nil && trace.WroteHeaderField != nil
-}
-
-func http2traceWroteHeaderField(trace *httptrace.ClientTrace, k, v string) {
-       if trace != nil && trace.WroteHeaderField != nil {
-               trace.WroteHeaderField(k, []string{v})
-       }
-}
-
 func http2traceGot1xxResponseFunc(trace *httptrace.ClientTrace) func(int, textproto.MIMEHeader) error {
        if trace != nil {
                return trace.Got1xxResponse
@@ -11345,7 +10958,7 @@ func http2encodeHeaders(enc *hpack.Encoder, h Header, keys []string) {
        }
        for _, k := range keys {
                vv := h[k]
-               k, ascii := http2lowerHeader(k)
+               k, ascii := httpcommon.LowerHeader(k)
                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).
index 32ff7e2008a84595a8c02713619982044d6794e9..e1e9eea0cefe93ef1e3d670ca7a34cfa16071775 100644 (file)
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:generate bundle -o=h2_bundle.go -prefix=http2 -tags=!nethttpomithttp2 golang.org/x/net/http2
+//go:generate bundle -o=h2_bundle.go -prefix=http2 -tags=!nethttpomithttp2 -import=golang.org/x/net/internal/httpcommon=net/http/internal/httpcommon golang.org/x/net/http2
 
 package http
 
index 5aba3ed5a6e5363b4a9db91cb9b642b7a037c45b..c12bbedac986db5995c2b6a2b5ce9406a3de5b56 100644 (file)
@@ -164,7 +164,9 @@ func TestNoUnicodeStrings(t *testing.T) {
                }
                if !strings.HasSuffix(path, ".go") ||
                        strings.HasSuffix(path, "_test.go") ||
-                       path == "h2_bundle.go" || d.IsDir() {
+                       path == "h2_bundle.go" ||
+                       path == "internal/httpcommon/httpcommon.go" ||
+                       d.IsDir() {
                        return nil
                }
 
diff --git a/src/net/http/internal/httpcommon/httpcommon.go b/src/net/http/internal/httpcommon/httpcommon.go
new file mode 100644 (file)
index 0000000..c9f1778
--- /dev/null
@@ -0,0 +1,532 @@
+// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.
+//go:generate bundle -prefix= -o=httpcommon.go golang.org/x/net/internal/httpcommon
+
+package httpcommon
+
+import (
+       "context"
+       "errors"
+       "fmt"
+       "net/http/httptrace"
+       "net/textproto"
+       "net/url"
+       "sort"
+       "strconv"
+       "strings"
+       "sync"
+
+       "golang.org/x/net/http/httpguts"
+       "golang.org/x/net/http2/hpack"
+)
+
+// The HTTP protocols are defined in terms of ASCII, not Unicode. This file
+// contains helper functions which may use Unicode-aware functions which would
+// otherwise be unsafe and could introduce vulnerabilities if used improperly.
+
+// asciiEqualFold is strings.EqualFold, ASCII only. It reports whether s and t
+// are equal, ASCII-case-insensitively.
+func asciiEqualFold(s, t string) bool {
+       if len(s) != len(t) {
+               return false
+       }
+       for i := 0; i < len(s); i++ {
+               if lower(s[i]) != lower(t[i]) {
+                       return false
+               }
+       }
+       return true
+}
+
+// lower returns the ASCII lowercase version of b.
+func lower(b byte) byte {
+       if 'A' <= b && b <= 'Z' {
+               return b + ('a' - 'A')
+       }
+       return b
+}
+
+// isASCIIPrint returns whether s is ASCII and printable according to
+// https://tools.ietf.org/html/rfc20#section-4.2.
+func isASCIIPrint(s string) bool {
+       for i := 0; i < len(s); i++ {
+               if s[i] < ' ' || s[i] > '~' {
+                       return false
+               }
+       }
+       return true
+}
+
+// asciiToLower returns the lowercase version of s if s is ASCII and printable,
+// and whether or not it was.
+func asciiToLower(s string) (lower string, ok bool) {
+       if !isASCIIPrint(s) {
+               return "", false
+       }
+       return strings.ToLower(s), true
+}
+
+var (
+       commonBuildOnce   sync.Once
+       commonLowerHeader map[string]string // Go-Canonical-Case -> lower-case
+       commonCanonHeader map[string]string // lower-case -> Go-Canonical-Case
+)
+
+func buildCommonHeaderMapsOnce() {
+       commonBuildOnce.Do(buildCommonHeaderMaps)
+}
+
+func buildCommonHeaderMaps() {
+       common := []string{
+               "accept",
+               "accept-charset",
+               "accept-encoding",
+               "accept-language",
+               "accept-ranges",
+               "age",
+               "access-control-allow-credentials",
+               "access-control-allow-headers",
+               "access-control-allow-methods",
+               "access-control-allow-origin",
+               "access-control-expose-headers",
+               "access-control-max-age",
+               "access-control-request-headers",
+               "access-control-request-method",
+               "allow",
+               "authorization",
+               "cache-control",
+               "content-disposition",
+               "content-encoding",
+               "content-language",
+               "content-length",
+               "content-location",
+               "content-range",
+               "content-type",
+               "cookie",
+               "date",
+               "etag",
+               "expect",
+               "expires",
+               "from",
+               "host",
+               "if-match",
+               "if-modified-since",
+               "if-none-match",
+               "if-unmodified-since",
+               "last-modified",
+               "link",
+               "location",
+               "max-forwards",
+               "origin",
+               "proxy-authenticate",
+               "proxy-authorization",
+               "range",
+               "referer",
+               "refresh",
+               "retry-after",
+               "server",
+               "set-cookie",
+               "strict-transport-security",
+               "trailer",
+               "transfer-encoding",
+               "user-agent",
+               "vary",
+               "via",
+               "www-authenticate",
+               "x-forwarded-for",
+               "x-forwarded-proto",
+       }
+       commonLowerHeader = make(map[string]string, len(common))
+       commonCanonHeader = make(map[string]string, len(common))
+       for _, v := range common {
+               chk := textproto.CanonicalMIMEHeaderKey(v)
+               commonLowerHeader[chk] = v
+               commonCanonHeader[v] = chk
+       }
+}
+
+// LowerHeader returns the lowercase form of a header name,
+// used on the wire for HTTP/2 and HTTP/3 requests.
+func LowerHeader(v string) (lower string, ascii bool) {
+       buildCommonHeaderMapsOnce()
+       if s, ok := commonLowerHeader[v]; ok {
+               return s, true
+       }
+       return asciiToLower(v)
+}
+
+// CanonicalHeader canonicalizes a header name. (For example, "host" becomes "Host".)
+func CanonicalHeader(v string) string {
+       buildCommonHeaderMapsOnce()
+       if s, ok := commonCanonHeader[v]; ok {
+               return s
+       }
+       return textproto.CanonicalMIMEHeaderKey(v)
+}
+
+// CachedCanonicalHeader returns the canonical form of a well-known header name.
+func CachedCanonicalHeader(v string) (string, bool) {
+       buildCommonHeaderMapsOnce()
+       s, ok := commonCanonHeader[v]
+       return s, ok
+}
+
+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
+       }
+}
index 5ffa43e85ef3eae8f3294248fabeb23d09883e6d..791a7d8e874bae2921238e3312967d14eaea6b6f 100644 (file)
@@ -6,7 +6,7 @@ golang.org/x/crypto/cryptobyte
 golang.org/x/crypto/cryptobyte/asn1
 golang.org/x/crypto/internal/alias
 golang.org/x/crypto/internal/poly1305
-# golang.org/x/net v0.34.1-0.20250123000230-c72e89d6a9e4
+# golang.org/x/net v0.35.1-0.20250213222735-884432780bfd
 ## explicit; go 1.18
 golang.org/x/net/dns/dnsmessage
 golang.org/x/net/http/httpguts