request.go raw

   1  // Copyright 2025 The Go Authors. All rights reserved.
   2  // Use of this source code is governed by a BSD-style
   3  // license that can be found in the LICENSE file.
   4  
   5  package httpcommon
   6  
   7  import (
   8  	"context"
   9  	"errors"
  10  	"fmt"
  11  	"net/http/httptrace"
  12  	"net/textproto"
  13  	"net/url"
  14  	"sort"
  15  	"strconv"
  16  	"strings"
  17  
  18  	"golang.org/x/net/http/httpguts"
  19  	"golang.org/x/net/http2/hpack"
  20  )
  21  
  22  var (
  23  	ErrRequestHeaderListSize = errors.New("request header list larger than peer's advertised limit")
  24  )
  25  
  26  // Request is a subset of http.Request.
  27  // It'd be simpler to pass an *http.Request, of course, but we can't depend on net/http
  28  // without creating a dependency cycle.
  29  type Request struct {
  30  	URL                 *url.URL
  31  	Method              string
  32  	Host                string
  33  	Header              map[string][]string
  34  	Trailer             map[string][]string
  35  	ActualContentLength int64 // 0 means 0, -1 means unknown
  36  }
  37  
  38  // EncodeHeadersParam is parameters to EncodeHeaders.
  39  type EncodeHeadersParam struct {
  40  	Request Request
  41  
  42  	// AddGzipHeader indicates that an "accept-encoding: gzip" header should be
  43  	// added to the request.
  44  	AddGzipHeader bool
  45  
  46  	// PeerMaxHeaderListSize, when non-zero, is the peer's MAX_HEADER_LIST_SIZE setting.
  47  	PeerMaxHeaderListSize uint64
  48  
  49  	// DefaultUserAgent is the User-Agent header to send when the request
  50  	// neither contains a User-Agent nor disables it.
  51  	DefaultUserAgent string
  52  }
  53  
  54  // EncodeHeadersResult is the result of EncodeHeaders.
  55  type EncodeHeadersResult struct {
  56  	HasBody     bool
  57  	HasTrailers bool
  58  }
  59  
  60  // EncodeHeaders constructs request headers common to HTTP/2 and HTTP/3.
  61  // It validates a request and calls headerf with each pseudo-header and header
  62  // for the request.
  63  // The headerf function is called with the validated, canonicalized header name.
  64  func EncodeHeaders(ctx context.Context, param EncodeHeadersParam, headerf func(name, value string)) (res EncodeHeadersResult, _ error) {
  65  	req := param.Request
  66  
  67  	// Check for invalid connection-level headers.
  68  	if err := checkConnHeaders(req.Header); err != nil {
  69  		return res, err
  70  	}
  71  
  72  	if req.URL == nil {
  73  		return res, errors.New("Request.URL is nil")
  74  	}
  75  
  76  	host := req.Host
  77  	if host == "" {
  78  		host = req.URL.Host
  79  	}
  80  	host, err := httpguts.PunycodeHostPort(host)
  81  	if err != nil {
  82  		return res, err
  83  	}
  84  	if !httpguts.ValidHostHeader(host) {
  85  		return res, errors.New("invalid Host header")
  86  	}
  87  
  88  	// isNormalConnect is true if this is a non-extended CONNECT request.
  89  	isNormalConnect := false
  90  	var protocol string
  91  	if vv := req.Header[":protocol"]; len(vv) > 0 {
  92  		protocol = vv[0]
  93  	}
  94  	if req.Method == "CONNECT" && protocol == "" {
  95  		isNormalConnect = true
  96  	} else if protocol != "" && req.Method != "CONNECT" {
  97  		return res, errors.New("invalid :protocol header in non-CONNECT request")
  98  	}
  99  
 100  	// Validate the path, except for non-extended CONNECT requests which have no path.
 101  	var path string
 102  	if !isNormalConnect {
 103  		path = req.URL.RequestURI()
 104  		if !validPseudoPath(path) {
 105  			orig := path
 106  			path = strings.TrimPrefix(path, req.URL.Scheme+"://"+host)
 107  			if !validPseudoPath(path) {
 108  				if req.URL.Opaque != "" {
 109  					return res, fmt.Errorf("invalid request :path %q from URL.Opaque = %q", orig, req.URL.Opaque)
 110  				} else {
 111  					return res, fmt.Errorf("invalid request :path %q", orig)
 112  				}
 113  			}
 114  		}
 115  	}
 116  
 117  	// Check for any invalid headers+trailers and return an error before we
 118  	// potentially pollute our hpack state. (We want to be able to
 119  	// continue to reuse the hpack encoder for future requests)
 120  	if err := validateHeaders(req.Header); err != "" {
 121  		return res, fmt.Errorf("invalid HTTP header %s", err)
 122  	}
 123  	if err := validateHeaders(req.Trailer); err != "" {
 124  		return res, fmt.Errorf("invalid HTTP trailer %s", err)
 125  	}
 126  
 127  	trailers, err := commaSeparatedTrailers(req.Trailer)
 128  	if err != nil {
 129  		return res, err
 130  	}
 131  
 132  	enumerateHeaders := func(f func(name, value string)) {
 133  		// 8.1.2.3 Request Pseudo-Header Fields
 134  		// The :path pseudo-header field includes the path and query parts of the
 135  		// target URI (the path-absolute production and optionally a '?' character
 136  		// followed by the query production, see Sections 3.3 and 3.4 of
 137  		// [RFC3986]).
 138  		f(":authority", host)
 139  		m := req.Method
 140  		if m == "" {
 141  			m = "GET"
 142  		}
 143  		f(":method", m)
 144  		if !isNormalConnect {
 145  			f(":path", path)
 146  			f(":scheme", req.URL.Scheme)
 147  		}
 148  		if protocol != "" {
 149  			f(":protocol", protocol)
 150  		}
 151  		if trailers != "" {
 152  			f("trailer", trailers)
 153  		}
 154  
 155  		var didUA bool
 156  		for k, vv := range req.Header {
 157  			if asciiEqualFold(k, "host") || asciiEqualFold(k, "content-length") {
 158  				// Host is :authority, already sent.
 159  				// Content-Length is automatic, set below.
 160  				continue
 161  			} else if asciiEqualFold(k, "connection") ||
 162  				asciiEqualFold(k, "proxy-connection") ||
 163  				asciiEqualFold(k, "transfer-encoding") ||
 164  				asciiEqualFold(k, "upgrade") ||
 165  				asciiEqualFold(k, "keep-alive") {
 166  				// Per 8.1.2.2 Connection-Specific Header
 167  				// Fields, don't send connection-specific
 168  				// fields. We have already checked if any
 169  				// are error-worthy so just ignore the rest.
 170  				continue
 171  			} else if asciiEqualFold(k, "user-agent") {
 172  				// Match Go's http1 behavior: at most one
 173  				// User-Agent. If set to nil or empty string,
 174  				// then omit it. Otherwise if not mentioned,
 175  				// include the default (below).
 176  				didUA = true
 177  				if len(vv) < 1 {
 178  					continue
 179  				}
 180  				vv = vv[:1]
 181  				if vv[0] == "" {
 182  					continue
 183  				}
 184  			} else if asciiEqualFold(k, "cookie") {
 185  				// Per 8.1.2.5 To allow for better compression efficiency, the
 186  				// Cookie header field MAY be split into separate header fields,
 187  				// each with one or more cookie-pairs.
 188  				for _, v := range vv {
 189  					for {
 190  						p := strings.IndexByte(v, ';')
 191  						if p < 0 {
 192  							break
 193  						}
 194  						f("cookie", v[:p])
 195  						p++
 196  						// strip space after semicolon if any.
 197  						for p+1 <= len(v) && v[p] == ' ' {
 198  							p++
 199  						}
 200  						v = v[p:]
 201  					}
 202  					if len(v) > 0 {
 203  						f("cookie", v)
 204  					}
 205  				}
 206  				continue
 207  			} else if k == ":protocol" {
 208  				// :protocol pseudo-header was already sent above.
 209  				continue
 210  			}
 211  
 212  			for _, v := range vv {
 213  				f(k, v)
 214  			}
 215  		}
 216  		if shouldSendReqContentLength(req.Method, req.ActualContentLength) {
 217  			f("content-length", strconv.FormatInt(req.ActualContentLength, 10))
 218  		}
 219  		if param.AddGzipHeader {
 220  			f("accept-encoding", "gzip")
 221  		}
 222  		if !didUA {
 223  			f("user-agent", param.DefaultUserAgent)
 224  		}
 225  	}
 226  
 227  	// Do a first pass over the headers counting bytes to ensure
 228  	// we don't exceed cc.peerMaxHeaderListSize. This is done as a
 229  	// separate pass before encoding the headers to prevent
 230  	// modifying the hpack state.
 231  	if param.PeerMaxHeaderListSize > 0 {
 232  		hlSize := uint64(0)
 233  		enumerateHeaders(func(name, value string) {
 234  			hf := hpack.HeaderField{Name: name, Value: value}
 235  			hlSize += uint64(hf.Size())
 236  		})
 237  
 238  		if hlSize > param.PeerMaxHeaderListSize {
 239  			return res, ErrRequestHeaderListSize
 240  		}
 241  	}
 242  
 243  	trace := httptrace.ContextClientTrace(ctx)
 244  
 245  	// Header list size is ok. Write the headers.
 246  	enumerateHeaders(func(name, value string) {
 247  		name, ascii := LowerHeader(name)
 248  		if !ascii {
 249  			// Skip writing invalid headers. Per RFC 7540, Section 8.1.2, header
 250  			// field names have to be ASCII characters (just as in HTTP/1.x).
 251  			return
 252  		}
 253  
 254  		headerf(name, value)
 255  
 256  		if trace != nil && trace.WroteHeaderField != nil {
 257  			trace.WroteHeaderField(name, []string{value})
 258  		}
 259  	})
 260  
 261  	res.HasBody = req.ActualContentLength != 0
 262  	res.HasTrailers = trailers != ""
 263  	return res, nil
 264  }
 265  
 266  // IsRequestGzip reports whether we should add an Accept-Encoding: gzip header
 267  // for a request.
 268  func IsRequestGzip(method string, header map[string][]string, disableCompression bool) bool {
 269  	// TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere?
 270  	if !disableCompression &&
 271  		len(header["Accept-Encoding"]) == 0 &&
 272  		len(header["Range"]) == 0 &&
 273  		method != "HEAD" {
 274  		// Request gzip only, not deflate. Deflate is ambiguous and
 275  		// not as universally supported anyway.
 276  		// See: https://zlib.net/zlib_faq.html#faq39
 277  		//
 278  		// Note that we don't request this for HEAD requests,
 279  		// due to a bug in nginx:
 280  		//   http://trac.nginx.org/nginx/ticket/358
 281  		//   https://golang.org/issue/5522
 282  		//
 283  		// We don't request gzip if the request is for a range, since
 284  		// auto-decoding a portion of a gzipped document will just fail
 285  		// anyway. See https://golang.org/issue/8923
 286  		return true
 287  	}
 288  	return false
 289  }
 290  
 291  // checkConnHeaders checks whether req has any invalid connection-level headers.
 292  //
 293  // https://www.rfc-editor.org/rfc/rfc9114.html#section-4.2-3
 294  // https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.2-1
 295  //
 296  // Certain headers are special-cased as okay but not transmitted later.
 297  // For example, we allow "Transfer-Encoding: chunked", but drop the header when encoding.
 298  func checkConnHeaders(h map[string][]string) error {
 299  	if vv := h["Upgrade"]; len(vv) > 0 && (vv[0] != "" && vv[0] != "chunked") {
 300  		return fmt.Errorf("invalid Upgrade request header: %q", vv)
 301  	}
 302  	if vv := h["Transfer-Encoding"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && vv[0] != "chunked") {
 303  		return fmt.Errorf("invalid Transfer-Encoding request header: %q", vv)
 304  	}
 305  	if vv := h["Connection"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && !asciiEqualFold(vv[0], "close") && !asciiEqualFold(vv[0], "keep-alive")) {
 306  		return fmt.Errorf("invalid Connection request header: %q", vv)
 307  	}
 308  	return nil
 309  }
 310  
 311  func commaSeparatedTrailers(trailer map[string][]string) (string, error) {
 312  	keys := make([]string, 0, len(trailer))
 313  	for k := range trailer {
 314  		k = CanonicalHeader(k)
 315  		switch k {
 316  		case "Transfer-Encoding", "Trailer", "Content-Length":
 317  			return "", fmt.Errorf("invalid Trailer key %q", k)
 318  		}
 319  		keys = append(keys, k)
 320  	}
 321  	if len(keys) > 0 {
 322  		sort.Strings(keys)
 323  		return strings.Join(keys, ","), nil
 324  	}
 325  	return "", nil
 326  }
 327  
 328  // validPseudoPath reports whether v is a valid :path pseudo-header
 329  // value. It must be either:
 330  //
 331  //   - a non-empty string starting with '/'
 332  //   - the string '*', for OPTIONS requests.
 333  //
 334  // For now this is only used a quick check for deciding when to clean
 335  // up Opaque URLs before sending requests from the Transport.
 336  // See golang.org/issue/16847
 337  //
 338  // We used to enforce that the path also didn't start with "//", but
 339  // Google's GFE accepts such paths and Chrome sends them, so ignore
 340  // that part of the spec. See golang.org/issue/19103.
 341  func validPseudoPath(v string) bool {
 342  	return (len(v) > 0 && v[0] == '/') || v == "*"
 343  }
 344  
 345  func validateHeaders(hdrs map[string][]string) string {
 346  	for k, vv := range hdrs {
 347  		if !httpguts.ValidHeaderFieldName(k) && k != ":protocol" {
 348  			return fmt.Sprintf("name %q", k)
 349  		}
 350  		for _, v := range vv {
 351  			if !httpguts.ValidHeaderFieldValue(v) {
 352  				// Don't include the value in the error,
 353  				// because it may be sensitive.
 354  				return fmt.Sprintf("value for header %q", k)
 355  			}
 356  		}
 357  	}
 358  	return ""
 359  }
 360  
 361  // shouldSendReqContentLength reports whether we should send
 362  // a "content-length" request header. This logic is basically a copy of the net/http
 363  // transferWriter.shouldSendContentLength.
 364  // The contentLength is the corrected contentLength (so 0 means actually 0, not unknown).
 365  // -1 means unknown.
 366  func shouldSendReqContentLength(method string, contentLength int64) bool {
 367  	if contentLength > 0 {
 368  		return true
 369  	}
 370  	if contentLength < 0 {
 371  		return false
 372  	}
 373  	// For zero bodies, whether we send a content-length depends on the method.
 374  	// It also kinda doesn't matter for http2 either way, with END_STREAM.
 375  	switch method {
 376  	case "POST", "PUT", "PATCH":
 377  		return true
 378  	default:
 379  		return false
 380  	}
 381  }
 382  
 383  // ServerRequestParam is parameters to NewServerRequest.
 384  type ServerRequestParam struct {
 385  	Method                  string
 386  	Scheme, Authority, Path string
 387  	Protocol                string
 388  	Header                  map[string][]string
 389  }
 390  
 391  // ServerRequestResult is the result of NewServerRequest.
 392  type ServerRequestResult struct {
 393  	// Various http.Request fields.
 394  	URL        *url.URL
 395  	RequestURI string
 396  	Trailer    map[string][]string
 397  
 398  	NeedsContinue bool // client provided an "Expect: 100-continue" header
 399  
 400  	// If the request should be rejected, this is a short string suitable for passing
 401  	// to the http2 package's CountError function.
 402  	// It might be a bit odd to return errors this way rather than returning an error,
 403  	// but this ensures we don't forget to include a CountError reason.
 404  	InvalidReason string
 405  }
 406  
 407  func NewServerRequest(rp ServerRequestParam) ServerRequestResult {
 408  	needsContinue := httpguts.HeaderValuesContainsToken(rp.Header["Expect"], "100-continue")
 409  	if needsContinue {
 410  		delete(rp.Header, "Expect")
 411  	}
 412  	// Merge Cookie headers into one "; "-delimited value.
 413  	if cookies := rp.Header["Cookie"]; len(cookies) > 1 {
 414  		rp.Header["Cookie"] = []string{strings.Join(cookies, "; ")}
 415  	}
 416  
 417  	// Setup Trailers
 418  	var trailer map[string][]string
 419  	for _, v := range rp.Header["Trailer"] {
 420  		for _, key := range strings.Split(v, ",") {
 421  			key = textproto.CanonicalMIMEHeaderKey(textproto.TrimString(key))
 422  			switch key {
 423  			case "Transfer-Encoding", "Trailer", "Content-Length":
 424  				// Bogus. (copy of http1 rules)
 425  				// Ignore.
 426  			default:
 427  				if trailer == nil {
 428  					trailer = make(map[string][]string)
 429  				}
 430  				trailer[key] = nil
 431  			}
 432  		}
 433  	}
 434  	delete(rp.Header, "Trailer")
 435  
 436  	// "':authority' MUST NOT include the deprecated userinfo subcomponent
 437  	// for "http" or "https" schemed URIs."
 438  	// https://www.rfc-editor.org/rfc/rfc9113.html#section-8.3.1-2.3.8
 439  	if strings.IndexByte(rp.Authority, '@') != -1 && (rp.Scheme == "http" || rp.Scheme == "https") {
 440  		return ServerRequestResult{
 441  			InvalidReason: "userinfo_in_authority",
 442  		}
 443  	}
 444  
 445  	var url_ *url.URL
 446  	var requestURI string
 447  	if rp.Method == "CONNECT" && rp.Protocol == "" {
 448  		url_ = &url.URL{Host: rp.Authority}
 449  		requestURI = rp.Authority // mimic HTTP/1 server behavior
 450  	} else {
 451  		var err error
 452  		url_, err = url.ParseRequestURI(rp.Path)
 453  		if err != nil {
 454  			return ServerRequestResult{
 455  				InvalidReason: "bad_path",
 456  			}
 457  		}
 458  		requestURI = rp.Path
 459  	}
 460  
 461  	return ServerRequestResult{
 462  		URL:           url_,
 463  		NeedsContinue: needsContinue,
 464  		RequestURI:    requestURI,
 465  		Trailer:       trailer,
 466  	}
 467  }
 468