| 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 | import net.ssl |
| 7 | import strings |
| 8 | |
| 9 | fn (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 | |
| 16 | fn 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). |
| 70 | fn (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. |
| 84 | fn (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 | |
| 117 | fn 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 | |
| 122 | fn (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). |
| 147 | fn (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 | |