| 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. |
| 4 | module 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). |
| 14 | const 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. |
| 19 | fn 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. |
| 29 | fn (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 | extra << H2HeaderField{lkey, val} |
| 52 | } |
| 53 | } |
| 54 | // Cookies: the request's own cookie map plus any Cookie header values, |
| 55 | // joined into one field (RFC 7540 Section 8.1.2.5 also allows splitting). |
| 56 | mut cookie_parts := []string{} |
| 57 | for k, v in req.cookies { |
| 58 | cookie_parts << '${k}=${v}' |
| 59 | } |
| 60 | for cv in header.values(.cookie) { |
| 61 | cookie_parts << cv |
| 62 | } |
| 63 | if cookie_parts.len > 0 { |
| 64 | extra << H2HeaderField{'cookie', cookie_parts.join('; ')} |
| 65 | } |
| 66 | return H2ClientRequest{ |
| 67 | method: method.str() |
| 68 | scheme: 'https' |
| 69 | authority: auth |
| 70 | path: path |
| 71 | headers: extra |
| 72 | body: data.bytes() |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | // h2_response_to_http converts an HTTP/2 response into a net.http Response, |
| 77 | // decoding any Content-Encoding the same way the HTTP/1.1 path does. |
| 78 | fn h2_response_to_http(h2resp H2ClientResponse) Response { |
| 79 | mut h := new_header() |
| 80 | for f in h2resp.headers { |
| 81 | h.add_custom(f.name, f.value) or {} |
| 82 | } |
| 83 | body := decode_response_body(h2resp.body.bytestr(), h.get(.content_encoding) or { '' }) |
| 84 | status := status_from_int(h2resp.status) |
| 85 | return Response{ |
| 86 | http_version: '2.0' |
| 87 | status_code: h2resp.status |
| 88 | status_msg: status.str() |
| 89 | header: h |
| 90 | body: body |
| 91 | } |
| 92 | } |
| 93 | |