v / vlib / net / http / backend.c.v
165 lines · 157 sloc · 6.63 KB · 065a450b86f6459b1e4398fe7b0594bbfcc2d691
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
6import net.ssl
7import strings
8
9fn (req &Request) ssl_do(port int, method Method, host_name string, path string, data string, header Header) !Response {
10 $if windows && !no_vschannel ? {
11 return vschannel_ssl_do(req, port, method, host_name, path, data, header)
12 }
13 return net_ssl_do(req, port, method, host_name, path, data, header)
14}
15
16fn net_ssl_do(req &Request, port int, method Method, host_name string, path string, data string, header Header) !Response {
17 mut retries := 0
18 req_headers := req.build_request_headers_with(method, host_name, port, path, data, header)
19 $if trace_http_request ? {
20 eprint('> ')
21 eprint(req_headers)
22 eprintln('')
23 }
24 // Advertise ALPN `h2` (with an `http/1.1` fallback) when HTTP/2 is enabled.
25 // This is the default for https requests, so ordinary get()/fetch() calls
26 // advertise ALPN and use HTTP/2 when the server selects it; callers can opt
27 // out with `enable_http2: false`. The HTTP/2 read path feeds the same
28 // streaming callbacks and honors the stop limits, so they do not force
29 // HTTP/1.1.
30 alpn := if req.enable_http2 { ['h2', 'http/1.1'] } else { []string{} }
31 for {
32 mut ssl_conn := ssl.new_ssl_conn(
33 verify: req.verify
34 cert: req.cert
35 cert_key: req.cert_key
36 validate: req.validate
37 in_memory_verification: req.in_memory_verification
38 alpn_protocols: alpn
39 )!
40 ssl_conn.dial(host_name, port) or {
41 retries++
42 if is_no_need_retry_error(err.code()) || retries >= req.max_retries {
43 return err
44 }
45 continue
46 }
47 // Propagate the request's read timeout into the SSL backend.
48 // Without this, mbedtls keeps its init-time default and openssl falls back to no
49 // timeout at all on a stalled socket — see issue surfaced by macOS arm64 + tcc CI hangs.
50 if req.read_timeout > 0 {
51 ssl_conn.set_read_timeout(req.read_timeout)
52 }
53 // If the server negotiated HTTP/2 via ALPN, speak it; otherwise fall
54 // back to the existing HTTP/1.1 path unchanged.
55 if req.enable_http2 && ssl_conn.negotiated_alpn() == 'h2' {
56 return req.h2_do(mut ssl_conn, method, host_name, port, path, data, header)!
57 }
58 return req.do_request(req_headers, mut ssl_conn)!
59 }
60 return error('http.net_ssl_do: exhausted retries')
61}
62
63// h2_do runs a single request over an HTTP/2 connection on an already-dialled,
64// ALPN-negotiated `h2` TLS socket, and returns the response as a net.http
65// Response. The request's streaming callbacks (on_progress / on_progress_body)
66// and stop limits are adapted onto the H2 chunk hook so they fire per DATA
67// frame, matching the HTTP/1.1 streaming semantics as closely as is possible
68// on the framed wire (on_progress receives DATA payloads rather than raw
69// network reads).
70fn (req &Request) h2_do(mut ssl_conn ssl.SSLConn, method Method, host_name string, port int, path string, data string, header Header) !Response {
71 defer {
72 ssl_conn.shutdown() or {}
73 }
74 mut conn := new_h2_conn(ssl_conn)
75 return req.h2_exchange(mut conn, method, host_name, port, path, data, header)!
76}
77
78// h2_exchange runs a single request over an already-established H2Conn and
79// converts the result to a net.http Response. It is transport-agnostic: the
80// caller is responsible for building the H2Conn over whatever ALPN-negotiated
81// `h2` transport (net.ssl on most platforms, SChannel on default Windows) and
82// for tearing it down afterwards. The request's streaming callbacks and stop
83// limits are adapted onto the H2 chunk hook, as documented on h2_do.
84fn (req &Request) h2_exchange(mut conn H2Conn, method Method, host_name string, port int, path string, data string, header Header) !Response {
85 base := req.to_h2_request(method, h2_authority(host_name, port), path, data, header)
86 on_progress := req.on_progress
87 on_progress_body := req.on_progress_body
88 mut on_data := H2DataFn(unsafe { nil })
89 if on_progress != unsafe { nil } || on_progress_body != unsafe { nil } {
90 on_data = fn [req, on_progress, on_progress_body] (chunk []u8, body_so_far u64, body_expected u64, status int) ! {
91 if on_progress != unsafe { nil } {
92 on_progress(req, chunk, body_so_far)!
93 }
94 if on_progress_body != unsafe { nil } {
95 on_progress_body(req, chunk, body_so_far, body_expected, status)!
96 }
97 }
98 }
99 h2req := H2ClientRequest{
100 method: base.method
101 scheme: base.scheme
102 authority: base.authority
103 path: base.path
104 headers: base.headers
105 body: base.body
106 on_data: on_data
107 stop_copying_limit: req.stop_copying_limit
108 stop_receiving_limit: req.stop_receiving_limit
109 }
110 h2resp := conn.do(h2req)!
111 if req.on_finish != unsafe { nil } {
112 req.on_finish(req, u64(h2resp.body.len))!
113 }
114 return h2_response_to_http(h2resp)
115}
116
117fn read_from_ssl_connection_cb(con voidptr, buf &u8, bufsize int) !int {
118 mut ssl_conn := unsafe { &ssl.SSLConn(con) }
119 return ssl_conn.socket_read_into_ptr(buf, bufsize)
120}
121
122fn (req &Request) do_request(req_headers string, mut ssl_conn ssl.SSLConn) !Response {
123 defer {
124 ssl_conn.shutdown() or {}
125 }
126 ssl_conn.write_string(req_headers) or { return err }
127 mut content := strings.new_builder(4096)
128 response_info := req.receive_all_data_from_cb_in_builder(mut content, voidptr(ssl_conn),
129 read_from_ssl_connection_cb)!
130 response_text := content.str()
131 $if trace_http_response ? {
132 eprint('< ')
133 eprint(response_text)
134 eprintln('')
135 }
136 if req.on_finish != unsafe { nil } {
137 req.on_finish(req, u64(response_text.len))!
138 }
139 return parse_received_response(response_text, response_info)
140}
141
142// h1_exchange_ssl sends an already-built HTTP/1.x request over an open TLS
143// connection and reads one response, leaving the connection open (unlike
144// do_request, which shuts the connection down). The bool result reports
145// whether the response was precisely framed, so the connection can safely
146// carry another request (see ReceivedResponseInfo.reusable).
147fn (req &Request) h1_exchange_ssl(mut ssl_conn ssl.SSLConn, raw string) !(Response, bool) {
148 ssl_conn.write_string(raw) or {
149 return error('http.transport: TLS connection write failed: ${err.msg()}')
150 }
151 mut content := strings.new_builder(4096)
152 response_info := req.receive_all_data_from_cb_in_builder(mut content, voidptr(ssl_conn),
153 read_from_ssl_connection_cb)!
154 response_text := content.str()
155 $if trace_http_response ? {
156 eprint('< ')
157 eprint(response_text)
158 eprintln('')
159 }
160 if req.on_finish != unsafe { nil } {
161 req.on_finish(req, u64(response_text.len))!
162 }
163 resp := parse_received_response(response_text, response_info)!
164 return resp, response_info.reusable
165}
166