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}