request.go

  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
  5package httpcommon
  6
  7import (
  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
 22var (
 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.
 29type 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.
 39type 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// EncodeHeadersParam is the result of EncodeHeaders.
 55type 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.
 64func 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.
268func 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.
298func 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
311func 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.
341func validPseudoPath(v string) bool {
342	return (len(v) > 0 && v[0] == '/') || v == "*"
343}
344
345func 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.
366func 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.
384type 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.
392type 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 returing an error,
403	// but this ensures we don't forget to include a CountError reason.
404	InvalidReason string
405}
406
407func 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}