vxx2 / vlib / net / http / h2_client.v
98 lines · 93 sloc · 3.38 KB · 89607c731290ee2c148c11541f73c40b23b00481
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module http
5
6// This file converts between net.http's Request/Response and the HTTP/2
7// client types in h2_conn.v. The actual transport wiring (ALPN negotiation on
8// the TLS socket) lives in backend.c.v; these helpers are pure and backend
9// agnostic, so they can be tested without a socket.
10
11// h2_hop_by_hop are header names that must not be forwarded on HTTP/2
12// (RFC 7540 Section 8.1.2.2), plus `host` (replaced by the :authority
13// pseudo-header) and `cookie` (handled specially below).
14const h2_hop_by_hop = ['connection', 'keep-alive', 'proxy-connection', 'transfer-encoding', 'upgrade',
15 'host', 'cookie']
16
17// h2_authority returns the :authority value for a host and port, omitting the
18// port for the default HTTPS port.
19fn h2_authority(host string, port int) string {
20 if port == 443 || port == 0 {
21 return host
22 }
23 return '${host}:${port}'
24}
25
26// to_h2_request builds an HTTP/2 request from this request. Header names are
27// lowercased, hop-by-hop headers are dropped, the Host header becomes the
28// :authority pseudo-header, and cookies are collapsed into a single field.
29fn (req &Request) to_h2_request(method Method, authority string, path string, data string, header Header) H2ClientRequest {
30 // An explicit Host header overrides the URL host, matching the HTTP/1.1
31 // path (used for virtual-host / host-override requests).
32 mut auth := authority
33 if host := header.get(.host) {
34 if host != '' {
35 auth = host
36 }
37 }
38 mut extra := []H2HeaderField{}
39 if !header.contains(.user_agent) && req.user_agent != '' {
40 extra << H2HeaderField{'user-agent', req.user_agent}
41 }
42 if data.len > 0 && !header.contains(.content_length) {
43 extra << H2HeaderField{'content-length', data.len.str()}
44 }
45 for key in header.keys() {
46 lkey := key.to_lower()
47 if lkey in h2_hop_by_hop {
48 continue
49 }
50 for val in header.custom_values(key) {
51 // RFC 9113 §8.2.2: TE may be sent on an HTTP/2 request, but MUST NOT
52 // carry any value other than 'trailers'. Drop a non-conformant TE
53 // rather than generate a malformed request.
54 if lkey == 'te' && val.trim_space().to_lower() != 'trailers' {
55 continue
56 }
57 extra << H2HeaderField{lkey, val}
58 }
59 }
60 // Cookies: the request's own cookie map plus any Cookie header values,
61 // joined into one field (RFC 7540 Section 8.1.2.5 also allows splitting).
62 mut cookie_parts := []string{}
63 for k, v in req.cookies {
64 cookie_parts << '${k}=${v}'
65 }
66 for cv in header.values(.cookie) {
67 cookie_parts << cv
68 }
69 if cookie_parts.len > 0 {
70 extra << H2HeaderField{'cookie', cookie_parts.join('; ')}
71 }
72 return H2ClientRequest{
73 method: method.str()
74 scheme: 'https'
75 authority: auth
76 path: path
77 headers: extra
78 body: data.bytes()
79 }
80}
81
82// h2_response_to_http converts an HTTP/2 response into a net.http Response,
83// decoding any Content-Encoding the same way the HTTP/1.1 path does.
84fn h2_response_to_http(h2resp H2ClientResponse) Response {
85 mut h := new_header()
86 for f in h2resp.headers {
87 h.add_custom(f.name, f.value) or {}
88 }
89 body := decode_response_body(h2resp.body.bytestr(), h.get(.content_encoding) or { '' })
90 status := status_from_int(h2resp.status)
91 return Response{
92 http_version: '2.0'
93 status_code: h2resp.status
94 status_msg: status.str()
95 header: h
96 body: body
97 }
98}
99