93 lines
3.1 KiB
V
93 lines
3.1 KiB
V
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
|
// Use of this source code is governed by an MIT license
|
|
// that can be found in the LICENSE file.
|
|
module http
|
|
|
|
// This file converts between net.http's Request/Response and the HTTP/2
|
|
// client types in h2_conn.v. The actual transport wiring (ALPN negotiation on
|
|
// the TLS socket) lives in backend.c.v; these helpers are pure and backend
|
|
// agnostic, so they can be tested without a socket.
|
|
|
|
// h2_hop_by_hop are header names that must not be forwarded on HTTP/2
|
|
// (RFC 7540 Section 8.1.2.2), plus `host` (replaced by the :authority
|
|
// pseudo-header) and `cookie` (handled specially below).
|
|
const h2_hop_by_hop = ['connection', 'keep-alive', 'proxy-connection', 'transfer-encoding', 'upgrade',
|
|
'host', 'cookie']
|
|
|
|
// h2_authority returns the :authority value for a host and port, omitting the
|
|
// port for the default HTTPS port.
|
|
fn h2_authority(host string, port int) string {
|
|
if port == 443 || port == 0 {
|
|
return host
|
|
}
|
|
return '${host}:${port}'
|
|
}
|
|
|
|
// to_h2_request builds an HTTP/2 request from this request. Header names are
|
|
// lowercased, hop-by-hop headers are dropped, the Host header becomes the
|
|
// :authority pseudo-header, and cookies are collapsed into a single field.
|
|
fn (req &Request) to_h2_request(method Method, authority string, path string, data string, header Header) H2ClientRequest {
|
|
// An explicit Host header overrides the URL host, matching the HTTP/1.1
|
|
// path (used for virtual-host / host-override requests).
|
|
mut auth := authority
|
|
if host := header.get(.host) {
|
|
if host != '' {
|
|
auth = host
|
|
}
|
|
}
|
|
mut extra := []H2HeaderField{}
|
|
if !header.contains(.user_agent) && req.user_agent != '' {
|
|
extra << H2HeaderField{'user-agent', req.user_agent}
|
|
}
|
|
if data.len > 0 && !header.contains(.content_length) {
|
|
extra << H2HeaderField{'content-length', data.len.str()}
|
|
}
|
|
for key in header.keys() {
|
|
lkey := key.to_lower()
|
|
if lkey in h2_hop_by_hop {
|
|
continue
|
|
}
|
|
for val in header.custom_values(key) {
|
|
extra << H2HeaderField{lkey, val}
|
|
}
|
|
}
|
|
// Cookies: the request's own cookie map plus any Cookie header values,
|
|
// joined into one field (RFC 7540 Section 8.1.2.5 also allows splitting).
|
|
mut cookie_parts := []string{}
|
|
for k, v in req.cookies {
|
|
cookie_parts << '${k}=${v}'
|
|
}
|
|
for cv in header.values(.cookie) {
|
|
cookie_parts << cv
|
|
}
|
|
if cookie_parts.len > 0 {
|
|
extra << H2HeaderField{'cookie', cookie_parts.join('; ')}
|
|
}
|
|
return H2ClientRequest{
|
|
method: method.str()
|
|
scheme: 'https'
|
|
authority: auth
|
|
path: path
|
|
headers: extra
|
|
body: data.bytes()
|
|
}
|
|
}
|
|
|
|
// h2_response_to_http converts an HTTP/2 response into a net.http Response,
|
|
// decoding any Content-Encoding the same way the HTTP/1.1 path does.
|
|
fn h2_response_to_http(h2resp H2ClientResponse) Response {
|
|
mut h := new_header()
|
|
for f in h2resp.headers {
|
|
h.add_custom(f.name, f.value) or {}
|
|
}
|
|
body := decode_response_body(h2resp.body.bytestr(), h.get(.content_encoding) or { '' })
|
|
status := status_from_int(h2resp.status)
|
|
return Response{
|
|
http_version: '2.0'
|
|
status_code: h2resp.status
|
|
status_msg: status.str()
|
|
header: h
|
|
body: body
|
|
}
|
|
}
|